From 451d6a45ec81063a88fcc73d0010ce571df4a07d Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 7 Feb 2023 22:53:40 +0000 Subject: [PATCH] Import maildir-utils_1.8.14.orig.tar.gz [dgit import orig maildir-utils_1.8.14.orig.tar.gz] --- .editorconfig | 34 + .github/ISSUE_TEMPLATE/feature-request.md | 20 + .github/ISSUE_TEMPLATE/guile.md | 20 + .github/ISSUE_TEMPLATE/misc.md | 16 + .github/ISSUE_TEMPLATE/mu-bug-report.md | 20 + .github/ISSUE_TEMPLATE/mu4e-bug-report.md | 29 + .github/issue_template.md | 39 + .github/workflows/build-and-test.yml | 39 + .gitignore | 142 + .mailmap | 1 + AUTHORS | 1 + COPYING | 674 ++ ChangeLog | 1 + Makefile.am | 60 + Makefile.meson | 105 + NEWS | 2 + NEWS.org | 1190 +++ README.org | 96 + TODO | 159 + autogen.sh | 30 + build-aux/config.rpath | 0 build-aux/meson-install-info.sh | 14 + configure.ac | 320 + contrib/Makefile.am | 26 + contrib/mu-completion.zsh | 124 + contrib/mu-sexp-convert | 204 + contrib/mu.spec | 129 + gtest.mk | 34 + guile/Makefile.am | 97 + guile/compile-scm.in | 22 + guile/examples/Makefile.am | 23 + guile/examples/contacts-export | 85 + guile/examples/msg-graphs | 133 + guile/examples/mu-biff | 59 + guile/examples/org2mu4e | 78 + guile/fdl.texi | 451 + guile/meson.build | 113 + guile/mu-guile-message.cc | 484 + guile/mu-guile-message.hh | 34 + guile/mu-guile-message.x | 6 + guile/mu-guile.cc | 258 + guile/mu-guile.hh | 81 + guile/mu-guile.texi | 995 ++ guile/mu-guile.x | 4 + guile/mu.scm | 318 + guile/mu/Makefile.am | 26 + guile/mu/README | 207 + guile/mu/contact.scm | 4 + guile/mu/message.scm | 4 + guile/mu/part.scm | 4 + guile/mu/plot.scm | 80 + guile/mu/script.scm | 57 + guile/mu/stats.scm | 165 + guile/scripts/Makefile.am | 29 + guile/scripts/find-dups.scm | 119 + guile/scripts/msgs-count.scm | 40 + guile/scripts/msgs-per-day.scm | 49 + guile/scripts/msgs-per-hour.scm | 49 + guile/scripts/msgs-per-month.scm | 50 + guile/scripts/msgs-per-year-month.scm | 52 + guile/scripts/msgs-per-year.scm | 48 + guile/tests/meson.build | 31 + guile/tests/test-mu-guile.cc | 131 + guile/tests/test-mu-guile.scm | 124 + lib/Makefile.am | 119 + lib/doxyfile.in | 181 + lib/index/Makefile.am | 46 + lib/index/meson.build | 37 + lib/index/mu-indexer.cc | 456 + lib/index/mu-indexer.hh | 126 + lib/index/mu-scanner.cc | 232 + lib/index/mu-scanner.hh | 100 + lib/index/test-scanner.cc | 65 + lib/meson.build | 81 + lib/message/Makefile.am | 51 + lib/message/meson.build | 95 + lib/message/mu-contact.cc | 211 + lib/message/mu-contact.hh | 212 + lib/message/mu-document.cc | 513 + lib/message/mu-document.hh | 239 + lib/message/mu-fields.cc | 200 + lib/message/mu-fields.hh | 546 + lib/message/mu-flags.cc | 170 + lib/message/mu-flags.hh | 352 + lib/message/mu-message-file.cc | 207 + lib/message/mu-message-file.hh | 98 + lib/message/mu-message-part.cc | 188 + lib/message/mu-message-part.hh | 165 + lib/message/mu-message.cc | 838 ++ lib/message/mu-message.hh | 481 + lib/message/mu-mime-object.cc | 790 ++ lib/message/mu-mime-object.hh | 1377 +++ lib/message/mu-priority.cc | 76 + lib/message/mu-priority.hh | 129 + lib/message/test-mu-message.cc | 1068 ++ lib/mu-bookmarks.cc | 136 + lib/mu-bookmarks.hh | 76 + lib/mu-contacts-cache.cc | 511 + lib/mu-contacts-cache.hh | 159 + lib/mu-maildir.cc | 458 + lib/mu-maildir.hh | 123 + lib/mu-parser.cc | 506 + lib/mu-parser.hh | 106 + lib/mu-query-match-deciders.cc | 223 + lib/mu-query-match-deciders.hh | 76 + lib/mu-query-results.hh | 413 + lib/mu-query-threads.cc | 919 ++ lib/mu-query-threads.hh | 41 + lib/mu-query.cc | 306 + lib/mu-query.hh | 101 + lib/mu-runtime.cc | 117 + lib/mu-runtime.hh | 66 + lib/mu-script.cc | 375 + lib/mu-script.hh | 125 + lib/mu-server.cc | 1106 ++ lib/mu-server.hh | 86 + lib/mu-store.cc | 716 ++ lib/mu-store.hh | 473 + lib/mu-tokenizer.cc | 129 + lib/mu-tokenizer.hh | 139 + lib/mu-tree.hh | 159 + lib/mu-xapian.cc | 134 + lib/mu-xapian.hh | 39 + lib/tests/bench-indexer.cc | 550 + lib/tests/cjk/cur/test1 | 10 + lib/tests/cjk/cur/test2 | 10 + lib/tests/cjk/cur/test3 | 10 + lib/tests/cjk/cur/test4 | 10 + lib/tests/meson.build | 81 + lib/tests/test-indexer.cc | 69 + lib/tests/test-mu-container.cc | 80 + lib/tests/test-mu-maildir.cc | 577 + lib/tests/test-mu-msg-fields.cc | 126 + lib/tests/test-mu-msg.cc | 354 + lib/tests/test-mu-store-query.cc | 648 ++ lib/tests/test-mu-store.cc | 365 + lib/tests/test-parser.cc | 139 + lib/tests/test-query.cc | 102 + lib/tests/test-tokenizer.cc | 147 + .../cur/1220863042.12663_1.mindcrime!2,S | 146 + .../cur/1220863060.12663_3.mindcrime!2,S | 230 + .../cur/1220863087.12663_15.mindcrime!2,PS | 136 + .../cur/1220863087.12663_19.mindcrime!2,S | 77 + .../cur/1220863087.12663_5.mindcrime!2,S | 84 + .../cur/1220863087.12663_7.mindcrime!2,RS | 138 + .../cur/1252168370_3.14675.cthulhu!2,S | 21 + .../testdir/cur/1283599333.1840_11.cthulhu!2, | 16 + .../cur/1305664394.2171_402.cthulhu!2, | 17 + lib/tests/testdir/cur/encrypted!2,S | 56 + lib/tests/testdir/cur/multimime!2,FS | 27 + lib/tests/testdir/cur/multirecip!2,S | 11 + lib/tests/testdir/cur/signed!2,S | 36 + lib/tests/testdir/cur/signed-encrypted!2,S | 54 + lib/tests/testdir/cur/special!2,Sabc | 10 + .../testdir/new/1220863087.12663_21.mindcrime | 111 + .../testdir/new/1220863087.12663_23.mindcrime | 105 + .../testdir/new/1220863087.12663_25.mindcrime | 98 + .../testdir/new/1220863087.12663_9.mindcrime | 209 + lib/tests/testdir/tmp/1220863087.12663.ignore | 98 + lib/tests/testdir2/Foo/cur/arto.eml | 448 + lib/tests/testdir2/Foo/cur/fraiche.eml | 10 + lib/tests/testdir2/Foo/cur/mail5 | 625 ++ lib/tests/testdir2/Foo/new/.noindex | 0 lib/tests/testdir2/Foo/tmp/.noindex | 0 lib/tests/testdir2/bar/cur/181736.eml | 42 + lib/tests/testdir2/bar/cur/mail1 | 38 + lib/tests/testdir2/bar/cur/mail2 | 14 + lib/tests/testdir2/bar/cur/mail3 | 34 + lib/tests/testdir2/bar/cur/mail4 | 29 + lib/tests/testdir2/bar/cur/mail5 | 7 + lib/tests/testdir2/bar/cur/mail6 | 18 + lib/tests/testdir2/bar/new/.noindex | 0 lib/tests/testdir2/bar/tmp/.noindex | 0 lib/tests/testdir2/wom_bat/cur/atomic | 20 + lib/tests/testdir2/wom_bat/cur/rfc822.1 | 44 + lib/tests/testdir2/wom_bat/cur/rfc822.2 | 44 + .../testdir4/1220863042.12663_1.mindcrime!2,S | 146 + .../1220863087.12663_19.mindcrime!2,S | 77 + .../testdir4/1252168370_3.14675.cthulhu!2,S | 22 + .../testdir4/1283599333.1840_11.cthulhu!2, | 15 + .../testdir4/1305664394.2171_402.cthulhu!2, | 17 + lib/tests/testdir4/181736.eml | 42 + lib/tests/testdir4/encrypted!2,S | 57 + lib/tests/testdir4/mail1 | 38 + lib/tests/testdir4/mail5 | 624 ++ lib/tests/testdir4/multimime!2,FS | 27 + lib/tests/testdir4/signed!2,S | 36 + lib/tests/testdir4/signed-bad!2,S | 35 + lib/tests/testdir4/signed-encrypted!2,S | 54 + lib/tests/testdir4/special!2,Sabc | 10 + lib/thirdparty/Makefile.am | 22 + lib/thirdparty/expected.hpp | 2326 +++++ lib/thirdparty/optional.hpp | 2063 ++++ lib/thirdparty/tabulate.hpp | 9235 +++++++++++++++++ lib/tokenize.cc | 38 + lib/utils/Makefile.am | 74 + lib/utils/meson.build | 42 + lib/utils/mu-async-queue.hh | 199 + lib/utils/mu-command-parser.cc | 204 + lib/utils/mu-command-parser.hh | 180 + lib/utils/mu-error.hh | 174 + lib/utils/mu-logger.cc | 182 + lib/utils/mu-logger.hh | 74 + lib/utils/mu-option.cc | 32 + lib/utils/mu-option.hh | 60 + lib/utils/mu-readline.cc | 135 + lib/utils/mu-readline.hh | 61 + lib/utils/mu-result.hh | 157 + lib/utils/mu-sexp.cc | 270 + lib/utils/mu-sexp.hh | 447 + lib/utils/mu-test-utils.cc | 150 + lib/utils/mu-test-utils.hh | 129 + lib/utils/mu-util.c | 468 + lib/utils/mu-util.h | 349 + lib/utils/mu-utils-format.hh | 61 + lib/utils/mu-utils.cc | 638 ++ lib/utils/mu-utils.hh | 459 + lib/utils/mu-xapian-utils.hh | 86 + lib/utils/tests/meson.build | 45 + lib/utils/tests/test-command-parser.cc | 149 + lib/utils/tests/test-mu-str.c | 169 + lib/utils/tests/test-mu-util.c | 286 + lib/utils/tests/test-option.cc | 59 + lib/utils/tests/test-sexp.cc | 190 + lib/utils/tests/test-utils.cc | 322 + m4/Makefile.am | 47 + m4/ax_ac_append_to_file.m4 | 32 + m4/ax_ac_print_to_file.m4 | 32 + m4/ax_add_am_macro_static.m4 | 28 + m4/ax_am_macros_static.m4 | 38 + m4/ax_append_compile_flags.m4 | 46 + m4/ax_append_flag.m4 | 50 + m4/ax_append_link_flags.m4 | 44 + m4/ax_check_compile_flag.m4 | 53 + m4/ax_check_enable_debug.m4 | 124 + m4/ax_check_gnu_make.m4 | 95 + m4/ax_check_link_flag.m4 | 53 + m4/ax_code_coverage.m4 | 272 + m4/ax_compiler_flags.m4 | 158 + m4/ax_compiler_flags_cflags.m4 | 161 + m4/ax_compiler_flags_cxxflags.m4 | 136 + m4/ax_compiler_flags_gir.m4 | 60 + m4/ax_compiler_flags_ldflags.m4 | 111 + m4/ax_cxx_compile_stdcxx.m4 | 948 ++ m4/ax_cxx_compile_stdcxx_17.m4 | 35 + m4/ax_file_escapes.m4 | 30 + m4/ax_is_release.m4 | 80 + m4/ax_lib_readline.m4 | 109 + m4/ax_require_defined.m4 | 37 + m4/ax_valgrind_check.m4 | 239 + m4/guile.m4 | 397 + m4/host-cpu-c-abi.m4 | 675 ++ m4/lib-ld.m4 | 168 + m4/lib-link.m4 | 774 ++ m4/lib-prefix.m4 | 249 + man/Makefile.am | 38 + man/meson.build | 36 + man/mu-add.1 | 47 + man/mu-bookmarks.5 | 36 + man/mu-cfind.1 | 133 + man/mu-easy.1 | 313 + man/mu-extract.1 | 100 + man/mu-fields.1 | 32 + man/mu-find.1 | 338 + man/mu-help.1 | 34 + man/mu-index.1 | 196 + man/mu-info.1 | 47 + man/mu-init.1 | 79 + man/mu-mkdir.1 | 45 + man/mu-query.7 | 361 + man/mu-remove.1 | 48 + man/mu-script.1 | 84 + man/mu-server.1 | 61 + man/mu-verify.1 | 69 + man/mu-view.1 | 53 + man/mu.1 | 188 + meson.build | 212 + meson_options.txt | 35 + mu/Makefile.am | 74 + mu/meson.build | 43 + mu/mu-cmd-cfind.cc | 439 + mu/mu-cmd-extract.cc | 205 + mu/mu-cmd-fields.cc | 151 + mu/mu-cmd-find.cc | 612 ++ mu/mu-cmd-index.cc | 137 + mu/mu-cmd-script.cc | 200 + mu/mu-cmd-server.cc | 176 + mu/mu-cmd.cc | 602 ++ mu/mu-cmd.hh | 108 + mu/mu-config.cc | 738 ++ mu/mu-config.hh | 246 + mu/mu-help-strings.awk | 58 + mu/mu-help-strings.txt | 200 + mu/mu-memcheck.in | 6 + mu/mu.cc | 125 + mu/tests/gmime-test.c | 260 + mu/tests/meson.build | 44 + mu/tests/test-mu-cmd-cfind.cc | 345 + mu/tests/test-mu-cmd.cc | 879 ++ mu/tests/test-mu-query.cc | 674 ++ mu4e/Makefile.am | 68 + mu4e/TODO | 19 + mu4e/fdl.texi | 451 + mu4e/meson.build | 124 + mu4e/mu4e-about.org | 15 + mu4e/mu4e-actions.el | 252 + mu4e/mu4e-bookmarks.el | 133 + mu4e/mu4e-compose.el | 945 ++ mu4e/mu4e-config.el.in | 12 + mu4e/mu4e-contacts.el | 298 + mu4e/mu4e-context.el | 238 + mu4e/mu4e-contrib.el | 185 + mu4e/mu4e-draft.el | 771 ++ mu4e/mu4e-folders.el | 341 + mu4e/mu4e-headers.el | 1871 ++++ mu4e/mu4e-helpers.el | 567 + mu4e/mu4e-icalendar.el | 229 + mu4e/mu4e-lists.el | 131 + mu4e/mu4e-main.el | 425 + mu4e/mu4e-mark.el | 471 + mu4e/mu4e-message.el | 234 + mu4e/mu4e-org.el | 143 + mu4e/mu4e-search.el | 473 + mu4e/mu4e-server.el | 602 ++ mu4e/mu4e-speedbar.el | 133 + mu4e/mu4e-update.el | 324 + mu4e/mu4e-vars.el | 364 + mu4e/mu4e-view.el | 1354 +++ mu4e/mu4e.el | 257 + mu4e/mu4e.texi | 4574 ++++++++ mu4e/obsolete/org-mu4e.el | 234 + mu4e/texinfo-klare.css | 228 + mu4e/version.texi.in | 4 + version.texi.in | 4 + 334 files changed, 83034 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/guile.md create mode 100644 .github/ISSUE_TEMPLATE/misc.md create mode 100644 .github/ISSUE_TEMPLATE/mu-bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/mu4e-bug-report.md create mode 100644 .github/issue_template.md create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .gitignore create mode 100644 .mailmap create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 Makefile.am create mode 100644 Makefile.meson create mode 100644 NEWS create mode 100644 NEWS.org create mode 100644 README.org create mode 100644 TODO create mode 100755 autogen.sh create mode 100644 build-aux/config.rpath create mode 100644 build-aux/meson-install-info.sh create mode 100644 configure.ac create mode 100644 contrib/Makefile.am create mode 100644 contrib/mu-completion.zsh create mode 100755 contrib/mu-sexp-convert create mode 100644 contrib/mu.spec create mode 100644 gtest.mk create mode 100644 guile/Makefile.am create mode 100644 guile/compile-scm.in create mode 100644 guile/examples/Makefile.am create mode 100755 guile/examples/contacts-export create mode 100755 guile/examples/msg-graphs create mode 100755 guile/examples/mu-biff create mode 100755 guile/examples/org2mu4e create mode 100644 guile/fdl.texi create mode 100644 guile/meson.build create mode 100644 guile/mu-guile-message.cc create mode 100644 guile/mu-guile-message.hh create mode 100644 guile/mu-guile-message.x create mode 100644 guile/mu-guile.cc create mode 100644 guile/mu-guile.hh create mode 100644 guile/mu-guile.texi create mode 100644 guile/mu-guile.x create mode 100644 guile/mu.scm create mode 100644 guile/mu/Makefile.am create mode 100644 guile/mu/README create mode 100644 guile/mu/contact.scm create mode 100644 guile/mu/message.scm create mode 100644 guile/mu/part.scm create mode 100644 guile/mu/plot.scm create mode 100644 guile/mu/script.scm create mode 100644 guile/mu/stats.scm create mode 100644 guile/scripts/Makefile.am create mode 100755 guile/scripts/find-dups.scm create mode 100755 guile/scripts/msgs-count.scm create mode 100755 guile/scripts/msgs-per-day.scm create mode 100755 guile/scripts/msgs-per-hour.scm create mode 100755 guile/scripts/msgs-per-month.scm create mode 100755 guile/scripts/msgs-per-year-month.scm create mode 100755 guile/scripts/msgs-per-year.scm create mode 100644 guile/tests/meson.build create mode 100644 guile/tests/test-mu-guile.cc create mode 100755 guile/tests/test-mu-guile.scm create mode 100644 lib/Makefile.am create mode 100644 lib/doxyfile.in create mode 100644 lib/index/Makefile.am create mode 100644 lib/index/meson.build create mode 100644 lib/index/mu-indexer.cc create mode 100644 lib/index/mu-indexer.hh create mode 100644 lib/index/mu-scanner.cc create mode 100644 lib/index/mu-scanner.hh create mode 100644 lib/index/test-scanner.cc create mode 100644 lib/meson.build create mode 100644 lib/message/Makefile.am create mode 100644 lib/message/meson.build create mode 100644 lib/message/mu-contact.cc create mode 100644 lib/message/mu-contact.hh create mode 100644 lib/message/mu-document.cc create mode 100644 lib/message/mu-document.hh create mode 100644 lib/message/mu-fields.cc create mode 100644 lib/message/mu-fields.hh create mode 100644 lib/message/mu-flags.cc create mode 100644 lib/message/mu-flags.hh create mode 100644 lib/message/mu-message-file.cc create mode 100644 lib/message/mu-message-file.hh create mode 100644 lib/message/mu-message-part.cc create mode 100644 lib/message/mu-message-part.hh create mode 100644 lib/message/mu-message.cc create mode 100644 lib/message/mu-message.hh create mode 100644 lib/message/mu-mime-object.cc create mode 100644 lib/message/mu-mime-object.hh create mode 100644 lib/message/mu-priority.cc create mode 100644 lib/message/mu-priority.hh create mode 100644 lib/message/test-mu-message.cc create mode 100644 lib/mu-bookmarks.cc create mode 100644 lib/mu-bookmarks.hh create mode 100644 lib/mu-contacts-cache.cc create mode 100644 lib/mu-contacts-cache.hh create mode 100644 lib/mu-maildir.cc create mode 100644 lib/mu-maildir.hh create mode 100644 lib/mu-parser.cc create mode 100644 lib/mu-parser.hh create mode 100644 lib/mu-query-match-deciders.cc create mode 100644 lib/mu-query-match-deciders.hh create mode 100644 lib/mu-query-results.hh create mode 100644 lib/mu-query-threads.cc create mode 100644 lib/mu-query-threads.hh create mode 100644 lib/mu-query.cc create mode 100644 lib/mu-query.hh create mode 100644 lib/mu-runtime.cc create mode 100644 lib/mu-runtime.hh create mode 100644 lib/mu-script.cc create mode 100644 lib/mu-script.hh create mode 100644 lib/mu-server.cc create mode 100644 lib/mu-server.hh create mode 100644 lib/mu-store.cc create mode 100644 lib/mu-store.hh create mode 100644 lib/mu-tokenizer.cc create mode 100644 lib/mu-tokenizer.hh create mode 100644 lib/mu-tree.hh create mode 100644 lib/mu-xapian.cc create mode 100644 lib/mu-xapian.hh create mode 100644 lib/tests/bench-indexer.cc create mode 100644 lib/tests/cjk/cur/test1 create mode 100644 lib/tests/cjk/cur/test2 create mode 100644 lib/tests/cjk/cur/test3 create mode 100644 lib/tests/cjk/cur/test4 create mode 100644 lib/tests/meson.build create mode 100644 lib/tests/test-indexer.cc create mode 100644 lib/tests/test-mu-container.cc create mode 100644 lib/tests/test-mu-maildir.cc create mode 100644 lib/tests/test-mu-msg-fields.cc create mode 100644 lib/tests/test-mu-msg.cc create mode 100644 lib/tests/test-mu-store-query.cc create mode 100644 lib/tests/test-mu-store.cc create mode 100644 lib/tests/test-parser.cc create mode 100644 lib/tests/test-query.cc create mode 100644 lib/tests/test-tokenizer.cc create mode 100644 lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S create mode 100644 lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S create mode 100644 lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS create mode 100644 lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S create mode 100644 lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S create mode 100644 lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS create mode 100644 lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S create mode 100644 lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2, create mode 100644 lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2, create mode 100644 lib/tests/testdir/cur/encrypted!2,S create mode 100644 lib/tests/testdir/cur/multimime!2,FS create mode 100644 lib/tests/testdir/cur/multirecip!2,S create mode 100644 lib/tests/testdir/cur/signed!2,S create mode 100644 lib/tests/testdir/cur/signed-encrypted!2,S create mode 100644 lib/tests/testdir/cur/special!2,Sabc create mode 100644 lib/tests/testdir/new/1220863087.12663_21.mindcrime create mode 100644 lib/tests/testdir/new/1220863087.12663_23.mindcrime create mode 100644 lib/tests/testdir/new/1220863087.12663_25.mindcrime create mode 100644 lib/tests/testdir/new/1220863087.12663_9.mindcrime create mode 100644 lib/tests/testdir/tmp/1220863087.12663.ignore create mode 100644 lib/tests/testdir2/Foo/cur/arto.eml create mode 100644 lib/tests/testdir2/Foo/cur/fraiche.eml create mode 100644 lib/tests/testdir2/Foo/cur/mail5 create mode 100644 lib/tests/testdir2/Foo/new/.noindex create mode 100644 lib/tests/testdir2/Foo/tmp/.noindex create mode 100644 lib/tests/testdir2/bar/cur/181736.eml create mode 100644 lib/tests/testdir2/bar/cur/mail1 create mode 100644 lib/tests/testdir2/bar/cur/mail2 create mode 100644 lib/tests/testdir2/bar/cur/mail3 create mode 100644 lib/tests/testdir2/bar/cur/mail4 create mode 100644 lib/tests/testdir2/bar/cur/mail5 create mode 100644 lib/tests/testdir2/bar/cur/mail6 create mode 100644 lib/tests/testdir2/bar/new/.noindex create mode 100644 lib/tests/testdir2/bar/tmp/.noindex create mode 100644 lib/tests/testdir2/wom_bat/cur/atomic create mode 100644 lib/tests/testdir2/wom_bat/cur/rfc822.1 create mode 100644 lib/tests/testdir2/wom_bat/cur/rfc822.2 create mode 100644 lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S create mode 100644 lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S create mode 100644 lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S create mode 100644 lib/tests/testdir4/1283599333.1840_11.cthulhu!2, create mode 100644 lib/tests/testdir4/1305664394.2171_402.cthulhu!2, create mode 100644 lib/tests/testdir4/181736.eml create mode 100644 lib/tests/testdir4/encrypted!2,S create mode 100644 lib/tests/testdir4/mail1 create mode 100644 lib/tests/testdir4/mail5 create mode 100644 lib/tests/testdir4/multimime!2,FS create mode 100644 lib/tests/testdir4/signed!2,S create mode 100644 lib/tests/testdir4/signed-bad!2,S create mode 100644 lib/tests/testdir4/signed-encrypted!2,S create mode 100644 lib/tests/testdir4/special!2,Sabc create mode 100644 lib/thirdparty/Makefile.am create mode 100644 lib/thirdparty/expected.hpp create mode 100644 lib/thirdparty/optional.hpp create mode 100644 lib/thirdparty/tabulate.hpp create mode 100644 lib/tokenize.cc create mode 100644 lib/utils/Makefile.am create mode 100644 lib/utils/meson.build create mode 100644 lib/utils/mu-async-queue.hh create mode 100644 lib/utils/mu-command-parser.cc create mode 100644 lib/utils/mu-command-parser.hh create mode 100644 lib/utils/mu-error.hh create mode 100644 lib/utils/mu-logger.cc create mode 100644 lib/utils/mu-logger.hh create mode 100644 lib/utils/mu-option.cc create mode 100644 lib/utils/mu-option.hh create mode 100644 lib/utils/mu-readline.cc create mode 100644 lib/utils/mu-readline.hh create mode 100644 lib/utils/mu-result.hh create mode 100644 lib/utils/mu-sexp.cc create mode 100644 lib/utils/mu-sexp.hh create mode 100644 lib/utils/mu-test-utils.cc create mode 100644 lib/utils/mu-test-utils.hh create mode 100644 lib/utils/mu-util.c create mode 100644 lib/utils/mu-util.h create mode 100644 lib/utils/mu-utils-format.hh create mode 100644 lib/utils/mu-utils.cc create mode 100644 lib/utils/mu-utils.hh create mode 100644 lib/utils/mu-xapian-utils.hh create mode 100644 lib/utils/tests/meson.build create mode 100644 lib/utils/tests/test-command-parser.cc create mode 100644 lib/utils/tests/test-mu-str.c create mode 100644 lib/utils/tests/test-mu-util.c create mode 100644 lib/utils/tests/test-option.cc create mode 100644 lib/utils/tests/test-sexp.cc create mode 100644 lib/utils/tests/test-utils.cc create mode 100644 m4/Makefile.am create mode 100644 m4/ax_ac_append_to_file.m4 create mode 100644 m4/ax_ac_print_to_file.m4 create mode 100644 m4/ax_add_am_macro_static.m4 create mode 100644 m4/ax_am_macros_static.m4 create mode 100644 m4/ax_append_compile_flags.m4 create mode 100644 m4/ax_append_flag.m4 create mode 100644 m4/ax_append_link_flags.m4 create mode 100644 m4/ax_check_compile_flag.m4 create mode 100644 m4/ax_check_enable_debug.m4 create mode 100644 m4/ax_check_gnu_make.m4 create mode 100644 m4/ax_check_link_flag.m4 create mode 100644 m4/ax_code_coverage.m4 create mode 100644 m4/ax_compiler_flags.m4 create mode 100644 m4/ax_compiler_flags_cflags.m4 create mode 100644 m4/ax_compiler_flags_cxxflags.m4 create mode 100644 m4/ax_compiler_flags_gir.m4 create mode 100644 m4/ax_compiler_flags_ldflags.m4 create mode 100644 m4/ax_cxx_compile_stdcxx.m4 create mode 100644 m4/ax_cxx_compile_stdcxx_17.m4 create mode 100644 m4/ax_file_escapes.m4 create mode 100644 m4/ax_is_release.m4 create mode 100644 m4/ax_lib_readline.m4 create mode 100644 m4/ax_require_defined.m4 create mode 100644 m4/ax_valgrind_check.m4 create mode 100644 m4/guile.m4 create mode 100644 m4/host-cpu-c-abi.m4 create mode 100644 m4/lib-ld.m4 create mode 100644 m4/lib-link.m4 create mode 100644 m4/lib-prefix.m4 create mode 100644 man/Makefile.am create mode 100644 man/meson.build create mode 100644 man/mu-add.1 create mode 100644 man/mu-bookmarks.5 create mode 100644 man/mu-cfind.1 create mode 100644 man/mu-easy.1 create mode 100644 man/mu-extract.1 create mode 100644 man/mu-fields.1 create mode 100644 man/mu-find.1 create mode 100644 man/mu-help.1 create mode 100644 man/mu-index.1 create mode 100644 man/mu-info.1 create mode 100644 man/mu-init.1 create mode 100644 man/mu-mkdir.1 create mode 100644 man/mu-query.7 create mode 100644 man/mu-remove.1 create mode 100644 man/mu-script.1 create mode 100644 man/mu-server.1 create mode 100644 man/mu-verify.1 create mode 100644 man/mu-view.1 create mode 100644 man/mu.1 create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 mu/Makefile.am create mode 100644 mu/meson.build create mode 100644 mu/mu-cmd-cfind.cc create mode 100644 mu/mu-cmd-extract.cc create mode 100644 mu/mu-cmd-fields.cc create mode 100644 mu/mu-cmd-find.cc create mode 100644 mu/mu-cmd-index.cc create mode 100644 mu/mu-cmd-script.cc create mode 100644 mu/mu-cmd-server.cc create mode 100644 mu/mu-cmd.cc create mode 100644 mu/mu-cmd.hh create mode 100644 mu/mu-config.cc create mode 100644 mu/mu-config.hh create mode 100644 mu/mu-help-strings.awk create mode 100644 mu/mu-help-strings.txt create mode 100644 mu/mu-memcheck.in create mode 100644 mu/mu.cc create mode 100644 mu/tests/gmime-test.c create mode 100644 mu/tests/meson.build create mode 100644 mu/tests/test-mu-cmd-cfind.cc create mode 100644 mu/tests/test-mu-cmd.cc create mode 100644 mu/tests/test-mu-query.cc create mode 100644 mu4e/Makefile.am create mode 100644 mu4e/TODO create mode 100644 mu4e/fdl.texi create mode 100644 mu4e/meson.build create mode 100644 mu4e/mu4e-about.org create mode 100644 mu4e/mu4e-actions.el create mode 100644 mu4e/mu4e-bookmarks.el create mode 100644 mu4e/mu4e-compose.el create mode 100644 mu4e/mu4e-config.el.in create mode 100644 mu4e/mu4e-contacts.el create mode 100644 mu4e/mu4e-context.el create mode 100644 mu4e/mu4e-contrib.el create mode 100644 mu4e/mu4e-draft.el create mode 100644 mu4e/mu4e-folders.el create mode 100644 mu4e/mu4e-headers.el create mode 100644 mu4e/mu4e-helpers.el create mode 100644 mu4e/mu4e-icalendar.el create mode 100644 mu4e/mu4e-lists.el create mode 100644 mu4e/mu4e-main.el create mode 100644 mu4e/mu4e-mark.el create mode 100644 mu4e/mu4e-message.el create mode 100644 mu4e/mu4e-org.el create mode 100644 mu4e/mu4e-search.el create mode 100644 mu4e/mu4e-server.el create mode 100644 mu4e/mu4e-speedbar.el create mode 100644 mu4e/mu4e-update.el create mode 100644 mu4e/mu4e-vars.el create mode 100644 mu4e/mu4e-view.el create mode 100644 mu4e/mu4e.el create mode 100644 mu4e/mu4e.texi create mode 100644 mu4e/obsolete/org-mu4e.el create mode 100644 mu4e/texinfo-klare.css create mode 100644 mu4e/version.texi.in create mode 100644 version.texi.in diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..824f406 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +#-*-mode:conf-*- +# editorconfig file (see EditorConfig.org), with some +# lowest-denominator settings that should work for many editors. + + +root = true # this is the top-level + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +# The "best" answer is "tabs-for-indentation; spaces for alignment". + +[*.{cc,cpp,hh,hpp}] +indent_style = tab +indent_size = 8 +max_line_length = 90 + +[*.{c,h}] +indent_style = tab +indent_size = 8 +max_line_length = 80 + +[configure.ac] +indent_style = tab +indent_size = 4 +max_line_length = 100 + +[Makefile.am] +indent_style = tab +indent_size = 8 +max_line_length = 100 diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..194f656 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,20 @@ +--- +name: Mu4e Feature request +about: Suggest an idea for this project +title: "[mu4e rfe]" +labels: rfe, mu4e, new +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/ISSUE_TEMPLATE/guile.md b/.github/ISSUE_TEMPLATE/guile.md new file mode 100644 index 0000000..020b849 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/guile.md @@ -0,0 +1,20 @@ +--- +name: Guile +about: mu-guile related item +title: "[guile]" +labels: new, guile +assignees: '' + +--- + +**Describe the item** +A clear and concise description of what you expected or wished to happen and what actually happened while using mu-guile. + +**To Reproduce** +Steps to reproduce the behavior. + +**Environment** +Please describe the versions of OS, Emacs, mu/mu4e etc. you are using. + +**Checklist** +- [ ] you are running either the latest 1.4.x release, or a 1.5.11+ development release (otherwise, please upgrade). diff --git a/.github/ISSUE_TEMPLATE/misc.md b/.github/ISSUE_TEMPLATE/misc.md new file mode 100644 index 0000000..7f942cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/misc.md @@ -0,0 +1,16 @@ +--- +name: Misc +about: Miscellaneous items you want to share +title: "[misc]" +labels: new +assignees: '' + +--- + +**Note**: for questions, please use the mailing-list: https://groups.google.com/g/mu-discuss + +**Describe the issue** +A clear and concise description, i.e. what you expected/desired to happen and what actually happened. + +**Environment** +If applicable, please describe the versions of OS, Emacs, mu etc. you are using. diff --git a/.github/ISSUE_TEMPLATE/mu-bug-report.md b/.github/ISSUE_TEMPLATE/mu-bug-report.md new file mode 100644 index 0000000..44604f4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/mu-bug-report.md @@ -0,0 +1,20 @@ +--- +name: Mu Bug Report +about: Create a report to help us improve +title: "[mu bug]" +labels: bug, mu, new +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is, what you expected to happen and what actually happened. + +**To Reproduce** +Detailed steps to reproduce the behavior. If this is about a specific (kind of) message, **always** attach an (anonymized as need) example message. + +**Environment** +Please describe the versions of OS, Emacs, mu etc. you are using. + +**Checklist** +- [ ] you are running either the latest 1.6.x release, or a 1.7.x development release (otherwise, please upgrade). diff --git a/.github/ISSUE_TEMPLATE/mu4e-bug-report.md b/.github/ISSUE_TEMPLATE/mu4e-bug-report.md new file mode 100644 index 0000000..738fb8e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/mu4e-bug-report.md @@ -0,0 +1,29 @@ +--- +name: Mu4e Bug Report +about: Create a report to help us improve +title: "[mu4e bug]" +labels: bug, mu4e, new +assignees: '' +--- + +**Describe the bug** + +Please provide a clear and concise description of what you expected to happen +and what actually happened. + +**How to Reproduce** + +Include the exact steps of what you were doing (commands executed etc.). Include +any relevant logs and outputs. + +If this is about a specific (kind of) message, attach an example message. (Open +the message, press `.` (`mu4e-view-raw-message`), then `C-x C-w` and attach. +Anonymize as needed, all that matters is that the issue still reproduces. + +**Environment** +Please describe the versions of OS, Emacs, mu/mu4e etc. you are using. + +**Checklist** +- [ ] you are running either the latest 1.6.x release, or a 1.8.x release (otherwise, please upgrade) +- [ ] you are running mu4e without any third-party extensions (otherwise, make sure you can reproduce without those) +- [ ] you have read all of the above diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..a684c39 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,39 @@ +# Important! Before filing an issue, please consider the following: + + * Ensure your mu/mu4e setup is no older than the latest stable release (1.6.x). + + * Disable any third-party mu4e extensions; this includes customizations like the ones in "Doom" / + "Evil" etc. + + * If a problem occurs with a certain (type of) message, attach an (anonymized) example of + such a message + + * Please provide some minimal steps to reproduce + + * Please follow the below template + + Thanks! + +## Expected or desired behavior + +Please describe the behavior you expect or want + +## Actual behavior + +Please describe the behavior you are actually seeing. + +For bug-reports, if applicable, include error messages, emacs stack traces, example messages +etc. Try to be as specific as possible - when do you see this happening? Does it happen always? +Sometimes? How often? + +## Steps to reproduce + +For bug-reports, please describe in as much detail as possible how one can reproduce the problem. + +If there's a problem with a specific (type of) message, please attach such a message to the report. + +## Versions of mu, mu4e/emacs, operating system etc. + +## Any other detail + +E.g. are you using the gnus-based message view? diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..60e5271 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,39 @@ +name: Build & run tests + +on: + - push + - pull_request + +jobs: + build: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + + steps: + - uses: actions/checkout@v2 + + - if: contains(matrix.os, 'ubuntu') + name: ubuntu-deps + run: | + sudo apt update + sudo apt-get install meson ninja-build libglib2.0-dev libxapian-dev libgmime-3.0-dev pkg-config + + - if: contains(matrix.os, 'macos') + name: macos-deps + run: | + brew install meson ninja libgpg-error libtool pkg-config glib gmime xapian + + - name: configure + run: ./autogen.sh -Dguile=disabled -Db_sanitize=address + + - name: build + run: make + + - name: test + run: make test-verbose-if-fail diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88ee280 --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +www/mu +mug +/mu/mu +/mu/mu-help-strings.h +mug2 +.desktop +*html +.deps +.libs +autom4te* +Makefile +Makefile.in +INSTALL +aclocal.m4 +config.* +configure +install-sh +depcomp +libtool +ltmain.sh + +# Added automatically by `autoreconf` +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 + +missing +nohup.out +vgdump +stamp-h1 +GPATH +GRTAGS +GSYMS +GTAGS +*.lo +*.o +*.la +*.x +*.go +*.gz +*.bz2 +\#* +*.aux +*.cp +*.fn +*.info +*.ky +*.log +*.pg +*.toc +*.tp +*.vr +*.elc +*.gcda +*.gcno +*.trs +*.exe +*.lib +aminclude_static.am +elisp-comp +elc-stamp +dummy.cc +msg2pdf +gmime-test +test-mu-cmd +test-mu-cmd-cfind +test-mu-contacts +test-mu-container +test-mu-date +test-mu-flags +test-mu-maildir +test-mu-msg +test-mu-msg-fields +test-mu-query +test-mu-runtime +test-mu-store +test-mu-str +test-mu-threads +test-mu-util +test-parser +test-tokenizer +test-utils +tokenize +test-command-parser +test-mu-utils +test-sexp-parser +test-scanner +/guile/tests/test-mu-guile + +mu4e-config.el +mu4e.pdf +texinfo.tex +texi.texi +*.tex +*.pdf +/www/auto/* +configure.lineno +/test.xml +/mu4e/mdate-sh +/mu4e/mu4e-about.el +/mu4e/stamp-vti +/mu4e/version.texi +/lib/doxyfile +/version.texi +/compile +build-aux/ +/TAGS +parse + +*_flymake.* +*_flymake_* +/perf.data +perf.data +perf.data.old +*vgdump +/lib/asan.log* +/man/mu-mfind.1 +/mu/mu-memcheck +mu-*-coverage +mu*tar.xz +compile_commands.json +/lib/utils/test-sexp +/lib/utils/test-option +/lib/test-mu-threader +/lib/test-mu-tokenizer +/lib/test-mu-parser +/lib/test-mu-query-threader +/lib/test-contacts +/lib/test-flags +/lib/test-maildir +/lib/test-msg +/lib/test-msg-fields +/lib/test-query +/lib/test-store +/lib/test-threader +/mu/test-cmd +/mu/test-cmd-cfind +/mu/test-query +/mu/test-threads +/lib/test-threads diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..3a54641 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Dirk-Jan C. Binnema diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..3a54641 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Dirk-Jan C. Binnema diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..eee38d2 --- /dev/null +++ b/ChangeLog @@ -0,0 +1 @@ +See NEWS.org diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..124fb47 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,60 @@ +## Copyright (C) 2008-2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +if BUILD_GUILE +guile=guile +else +guile= +endif + +if BUILD_MU4E +mu4e=mu4e +else +mu4e= +endif + +SUBDIRS=m4 man lib $(guile) mu $(mu4e) contrib + +ACLOCAL_AMFLAGS=-I m4 + +# so we can say 'make test' +check: test cleanupnote + +cleanupnote: + @echo -e "\nNote: you can remove the mu-test- dir in your tempdir" + @echo "after 'make check' has finished." + +tags: + gtags + +EXTRA_DIST= \ + TODO \ + README.org \ + gtest.mk \ + NEWS \ + NEWS.org \ + autogen.sh + +doc_DATA = \ + NEWS.org + +include $(top_srcdir)/aminclude_static.am + +CODE_COVERAGE_IGNORE_PATTERN= \ + '/usr/*' \ + '*test-*' diff --git a/Makefile.meson b/Makefile.meson new file mode 100644 index 0000000..411233d --- /dev/null +++ b/Makefile.meson @@ -0,0 +1,105 @@ +## Copyright (C) 2008-2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# Makefile with some useful targets for meson/ninja + +NINJA ?= ninja +BUILDDIR ?= $(CURDIR)/build +COVERAGE_BUILDDIR ?= $(CURDIR)/build-coverage +MESON ?= meson +V ?= 0 + +ifneq ($(V),0) + VERBOSE=--verbose +endif + + +.PHONY: all +.PHONY: check test test-verbose-if-fail test-valgrind test-helgrind +.PHONY: benchmark coverage +.PHONY: dist install clean distclean +.PHONY: mu4e-doc-html + +# MESON_FLAGS, e.g. "-Dreadline=enabled" + +# examples: +# 1. build with clang, and the thread-sanitizer +# make clean all MESON_FLAGS="-Db_sanitize=thread" CXX=clang++ CC=clang +all: $(BUILDDIR) + $(NINJA) -C $(BUILDDIR) $(VERBOSE) + +$(BUILDDIR): + $(MESON) $(MESON_FLAGS) $(BUILDDIR) + +check: test + +test: all + $(MESON) test $(VERBOSE) -C $(BUILDDIR) + + +install: $(BUILDDIR) + @cd $(BUILDDIR); $(MESON) install + +clean: + @rm -rf $(BUILDDIR) $(COVERAGE_BUILDDIR) + + +# +# below targets are just for development/testing/debugging. They may or +# may not work on your system. +# + +test-verbose-if-fail: all + @cd $(BUILDDIR); $(MESON) test || $(MESON) test --verbose + +test-valgrind: $(BUILDDIR) + @cd $(BUILDDIR); $(MESON) test \ + --wrap='valgrind --leak-check=full --error-exitcode=1' \ + --timeout-multiplier 100 + +# we do _not_ pass helgrind; but this seems to be a false-alarm +# https://gitlab.gnome.org/GNOME/glib/-/issues/2662 +# test-helgrind: $(BUILDDIR) +# @cd $(BUILDDIR); TEST=HELGRIND $(MESON) test \ +# --wrap='valgrind --tool=helgrind --error-exitcode=1' \ +# --timeout-multiplier 100 + +benchmark: $(BUILDDIR) + $(NINJA) -C $(BUILDDIR) benchmark + +$(COVERAGE_BUILDDIR): + $(MESON) -Db_coverage=true --buildtype=debug $(COVERAGE_BUILDDIR) + +covfile:=$(COVERAGE_BUILDDIR)/meson-logs/coverage.info + +# generate by hand, meson's built-ins are unflexible +coverage: $(COVERAGE_BUILDDIR) + $(NINJA) -C $(COVERAGE_BUILDDIR) test + lcov --capture --directory . --output-file $(covfile) + @lcov --remove $(covfile) '/usr/*' '*guile*' '*thirdparty*' '*/tests/*' '*mime-object*' --output $(covfile) + @lcov --remove $(covfile) '*mu/mu/*' --output $(covfile) + @mkdir -p $(COVERAGE_BUILDDIR)/meson-logs/coverage + @genhtml $(covfile) --output-directory $(COVERAGE_BUILDDIR)/meson-logs/coverage/ + @echo "coverage report at: file://$(COVERAGE_BUILDDIR)/meson-logs/coverage/index.html" +dist: $(BUILDDIR) + @cd $(BUILDDIR); $(MESON) dist + +distclean: clean + +HTMLPATH=${BUILDDIR}/mu4e/mu4e +mu4e-doc-html: + @mkdir -p ${HTMLPATH} && cp mu4e/texinfo-klare.css ${HTMLPATH} + @makeinfo --html --css-ref=texinfo-klare.css -o ${HTMLPATH} mu4e/mu4e.texi diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..23e9add --- /dev/null +++ b/NEWS @@ -0,0 +1,2 @@ +See NEWS.org + diff --git a/NEWS.org b/NEWS.org new file mode 100644 index 0000000..4ee4d3c --- /dev/null +++ b/NEWS.org @@ -0,0 +1,1190 @@ +#+STARTUP:showall +* NEWS (user visible changes & bigger non-visible ones) + +* 1.8 (released on June 25, 2022) + + (there are some changes in the installation procedure compared to 1.6.x; see + Installation below) + +*** mu + + - The server protocol (as used my mu4e) has seen a number of updates, to + allow for faster rendering. As before, there's no compatibility between + minor release numbers (1.4 vs 1.6 vs 1.8) nor within development series + (such as 1.7). However, within a stable release (such as all 1.6.x) the + protocol won't change (except if required to fix some severe bug; this + never happened in practice) + + - The ~processed~ number in the indexing statistics has been renamed into + ~checked~ and describes the number of message files considered for updating, + which is a bit more useful that the old value, which was more-or-less + synonymous with the ~updated~ number (which are the messages that got + (re)parsed / (re)added to the database. + + Basically, it counts all the messages for which we checked their timestamp. + + - The internals of the message handling in ~mu~ have been heavily reworked; + much of this is not immediately visible but is an enabler for some new + features. + + - instead of passing ~--muhome~, you can now also set an environment variable + ~MUHOME~. + + - the ~info~ command now includes information about the last indexing + operation and the last database change that took place; note that the + information may be slightly delayed due to database caching. + + - the ~verify~ command for checking signatures has been updated, and is more + informative + + - a new command ~fields~ provides information about the message fields and + flags for use in queries. The information is the same information that ~mu~ + uses and so stays up to date. + + - a new message field ~changed~, which refers to the time/date of the last + time a message was changed (the file ~ctime~) + + - new message flags ~personal~ to search for "personal" messages, which are + defined as a message with at least one personal contact, and ~calendar~ for + messages with calendar-invitations. + + - message sexps are now cached in the store, which makes delivering + sexp-based search results (as used by ~mu4e~) much faster. + + - Windows/MSYS support is deprecated; it doesn't work well (if at all) and + there's currently not sufficient developer interest/expertise to change + this. + +*** mu4e + + - the old mu4e-view is *gone*; only the gnus-based one remains. This allowed + for removing quite a bit of old code. + + - the mu4e headers rendering is much faster (a factor of 3+), which makes + displaying big results snappier. This required some updates in the headers + handling and in the server protocol. Separate from that, the cached + message sexps (see the ~mu~ section) make getting the results much faster. + This becomes esp. clear when there are a lot of query results. + + - "related" messages are now recognizable as such in the headers-view, with + their own face, ~mu4e-related-face~; by default with an italic slant. + + - For performance testing, you can set the variable + ~mu4e-headers-report-render-time~ to ~t~ and ~mu4e~ will report the + search/rendering speed of each query operation. + + - Removed header-fields ~:attachments~, ~:signature~, ~:encryption~ and + ~:user-agent~. They're obsolete with the Gnus-based message viewer. + + - The various "toggles" for the headers-view (full-search, include-related, + skip-duplicates, threading) were a bit hard to find and with non-obvious + key-bindings. For that, there is now ~mu4e-headers-toggle-setting~ (bound + to ~M~) to handle all of that. The toggles are also reflected in the + mode-line; so e.g. 'RTU' means we're including [R]elated messages, and show + [T]hreads, skip duplicates ([U]nique). + + - A new ~defcustom~, ~mu4e-view-open-program~ for starting the appropriate + program for a give file (e.g., ~xdg-open~). There are some reasonable + defaults for various systems. This can also be set to a function. + + - indexing happens in the background now and mu4e can interact with the + server while it is ongoing; this allows for using mu4e during lengthy + indexing operations. + + - ~mu4e-index-updated-hook~ now fires after indexing completed, regardless of + whether anything changed (before, it fired only if something changed). In + your hook-functions (or elsewhere) you can check if anything changed using + the new variable ~mu4e-index-update-status~. And note that ~processed~ has + been renamed into ~checked~, with a slightly different meaning, see the mu + section. + + - ~message-user-organization~ can now be used to set the ~Organization:~ + header. See its docstring for details. + + - ~mu4e-compose-context-switch~ no longer attempts to update the draft folder + (which turned out to be a little fragile). However, it has been updated to + automatically change the ~Organization:~ header, and attempts to update the + message signature. Also, there's a key-binding now: ~C-c ;~ + + - Changed the default for ~mu4e-compose-complete-only-after~ to 2018-01-01, + to filter out contacts not seen after that date. + + - As an additional measure to limit the number of contacts that mu4e loads + for auto-completions, there's ~mu4e-compose-complete-max~, to set a precise + numerical match (*before* any possible filtering). Set to ~nil~ (no maximum + by default). + + - Updated the "fancy" characters for some header fields. Added new ones for + personal and list messages. + + - Removed ~make-mu4e-bookmark~ which was obsoleted in version 1.3.9. + + - Add command ~mu4e-sexp-at-point~ for showing/hiding the s-expression for + the message-at-point. Useful for development / debugging. Bound to ~,~ in + headers and view mode. + + - undo is now supported across message-saves + + - a lot of the internals have been changed: + + - =mu4e= is slowly moving from using the '=~'= to the more common '=--'= + separator for private functions; i.e., =mu4e-foo= becomes =mu4e--foo=. + + - =mu4e-utils.el= had become a bit of a dumping ground for bits of code; + it's gone now, with the functionality move to topic-specific files -- + =mu4e-folders.el=, =mu4e-bookmarks.el=, =mu4e-update.el=, and included in + existing files. + + - the remaining common functionality has ended up in =mu4e-helpers.el= + + - =mu4e-search.el= takes the search-specific code from =mu4e-headers.el=, + and adds a minor-mode for the keybindings. + + - =mu4e-context.el= and =mu4e-update.el= also define minor modes with + keybindings, which saves a lot of code in the various views, since they + don't need explicitly bind all those function. + + - also =mu4e-vars.el= had become very big, we're refactoring the =defvar= / + =defcustom= declarations to the topic-specific files. + + - =mu4e-proc.el= has been renamed =mu4e-server.el=. + + - Between =mu= and =mu4e=, contact cells are now represented as a plist ~(:name + "Foo Bar" :email "foobar@example.com")~ rather than a cons-cell ~("Foo + Bar" . "foobar@example.com").~ + + If you have scripts depending on the old format, there's the + ~mu4e-contact-cons~ function which takes a news-style contact and yields + the old form. + + - Because of all these changes, it is recommended you remove older version + of ~mu4e~ before reinstalling. + +*** guile + + - the current guile support has been deprecated. It may be revamped at some + point, but will be different from the current one, which is to be removed + after 1.8 + +*** toys + + - the ~toys~ (~mug~) has been removed, as they no longer worked with the rest of + the code. + +** Installation + + - =mu= switched to the [[https://mesonbuild.com][meson]] build system by default. The existing =autotools= + is still available, but is to be removed after the 1.8 release. + + Using =meson= (which you may need to install), you can use something like + the following in the mu top source directory: + +#+BEGIN_SRC sh + $ meson build && ninja -C build +#+END_SRC + + - However, note that =autogen.sh= has been updated, and there's a + convenience =Makefile= with some useful targets, so you can also do: +#+BEGIN_SRC sh + $ ./autogen.sh && make # and optionally, 'sudo make install' +#+END_SRC + + - After that, either =ninja -C build= or =make= should be enough to rebuild + + - NOTE: development versions 1.7.18 - 17.7.25 had a bug where the mail file + names sometimes got misnamed (with some extra ':2,'). This can be restored + with something like: +#+begin_example + $ find ~/Maildir -name '*:2,*:*' | \ + sed "s/\(\([^:]*\)\(:2,\)\{1,\}\(:2,.*$\)\)/mv '\0' '\2\4'/" > rename.sh +#+end_example + (replace 'Maildir' with the path to your maildir) + + once this is done, do check the generated 'rename.sh' and after convincing + yourself it does the right thing, do +#+begin_example + $ sh rename.sh +#+end_example + after that, re-index. + + - Before installing, it is recommended that you *remove* any older versions + of ~mu~ and especially ~mu4e~, since they may conflict with the newer ones. + + - =mu= now requires C++17 support for building + + +** Contributor for this release + + - As per ~git~: c0dev0id, Christophe Troestler, Daniel Fleischer, Daniel Nagy, + Dirk-Jan C. Binnema, Dr. Rich Cordero, Kai von Fintel, Marcelo Henrique + Cerri, Nicholas Vollmer, PRESFIL, Tassilo Horn, Thierry Volpiatto, Yaman + Qalieh, Yuri D'Elia, Zero King + - And of course all the people filing issues, suggesting features and helping + out on the maling list. + + + +* 1.6 (released, as of July 27 2021) + + NOTE: After upgrading, you need to call ~mu init~, with your prefered parameters + before you can use ~mu~ / ~mu4e~. This is because the underlying database-schema + has changed. + +*** mu + + - Where available (and with suitably equiped ~libglib~), log to the ~systemd~ + journal instead of =~/.cache/mu.log=. Passing the ~--debug~ option to ~mu~ + increases the amount that is logged. + + - Follow symlinks in maildirs, and support moving messsages across + filesystems. Obviously, that is typically quite a bit slower than the + single-filesystem case, but can be still be useful. + + - Optionally provide readline support for the ~mu~ server (when in tty-mode) + + - Reworked the way mu generates s-expressions for mu4e; they are created + programmatically now instead of through string building. + + - The indexer (the part of mu that scans maildirs and updates the message + store) has been rewritten so it can work asynchronously and take advantage + of multiple cores. Note that for now, indexing in ~mu4e~ is still a blocking + operation. + + - Portability updates for dealing with non-POSIX systems, and in particular + VFAT filesystem, and building using Clang/libc++. + + - The personal addresses (as per ~--my-address=~ for ~mu init~) can now also + include regular expressions (basic POSIX); wrap the expression in ~/~, e.g., + ~--my-address='/.*@example.*/~'. + + - Modernized the querying/threading machinery; this makes some old code a + lot easier to understand and maintain, and even while not an explicit + goal, is also faster. + + - Experimental support for the Meson build system. + +*** mu4e + + - Use the gnus-based message viewer as the default; the new viewer has quite + a few extra features compared to the old, mu4e-specific one, such as + faster crypto, support for S/MIME, syntax-highlighting, calendar + invitations and more. + + The new view is superior in most ways, but if you still depend on + something from the old one, you can use: + #+begin_example + ;; set *before* loading mu4e; and restart emacs if you want to change it + ;; users of use-packag~ should can use the :init section for this. + (setq mu4e-view-use-old t) + #+end_example + + (The older variable ~mu4e-view-use-gnus~ with the opposite meaning is + obsolete now, and no longer in use). + + - Include maildir-shortcuts in the main-view with overall/unread counts, + similar to bookmarks, and with the same ~:hide~ and ~:hide-unread~ properties. + Note that for the latter, you need to update your maildir-shortcuts to the + new format, as explained in the ~mu4e-maildir-shortcuts~ docstring. + + You can set ~mu4e-main-hide-fully-read~ to hide any bookmarks/maildirs that + have no unread messages. + + - Add some more properties for use in capturing org-mode links to messages / + queries. See [[info:mu4e#Org-mode links][the mu4e manual]] for details. + + - Honor ~truncate-string-ellipsis~ so you can now use 'fancy' ellipses for + truncated strings with ~(setq truncate-string-ellipsis "…")~ + + - Add a variable ~mu4e-mu-debug~ which, when set to non-~nil,~ makes the ~mu~ + server log more verbosely (to ~mu.log~ or the journal) + + - Better alignment in headers-buffers; this looks nicer, but is also a bit + slower, hence you need to enable ~mu4e-headers-precise-alignment~ for this. + + - Support ~mu~'s new regexp-based personal addresses, and add + ~mu4e-personal-address-p~ to check whether a given string matches a personal + address. + + - TAB-Completion for writing ~mu~ queries + + - Switch the context for existing draft messages using + ~mu4e-compose-context-switch~ or ~C-c C-;~ in ~mu4e-compose-mode~. + +* 1.4 (released, as of April 18 2020) + +*** mu + + - mu now defaults to the [[https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html][XDG Base Directory Specification]] for the default + locations for various files. E.g. on Unix the mu database now lives under + ~~/.cache/mu/~ rather than ~~/.mu~. You can still use the old location by + passing ~--muhome=~/.mu~ to various ~mu~ commands, or setting ~(setq + mu4e-mu-home "~/.mu")~ for ~mu4e~. + + If your ~~/.cache~ is volatile (e.g., is cleared on reboot), you may want + use ~--muhome~. Some mailing-list dicussion suggest that's fairly rare + though. + + After upgrading, you may wish to delete the files in the old location to + recover some diskspace. + + - There's a new subcommand ~mu init~ to initialize the mu database, which + takes the ~--maildir~ and ~--my-address~ parameters that ~index~ used to take. + These parameters are persistent so ~index~ does not need (or accept) them + anymore. ~mu4e~ now depends on those parameters. + + ~init~ only needs to be run once or when changing these parameters. That + implies that you need to re-index after changing these parameters. The + ~.noupdate~ files are ignored when indexing the first time after ~mu init~ (or + in general, when the database is empty). + + - There is another new subcommand ~mu info~ to get information about the mu + database, the personal addresses etc. + + - The contacts cache (which is used by ~mu cfind~ and ~mu4e~'s + contact-completion) is now stored as part of the Xapian database rather + than as a separate file. + + - The ~--xbatchsize~ and ~--autoupgrade~ options for indexing are gone; both are + determined implicitly now. + +*** mu4e + + - ~mu4e~ no longer uses the ~mu4e-maildir~ and ~mu4e-user-mail-address-list~ + variables; instead it uses the information it gets from ~mu~ (see the ~mu~ + section above). If you have a non-default ~mu4e-mu-home~, make sure to set + it before ~mu4e~ starts. + + It is strongly recommended that you run ~mu init~ with the appropriate + parameters to (re)initialize the Xapian database, as mentioned in the + mu-section above. + + The main screen shows your address(es), and issues a warning if + ~user-email-address~ is not part of that (and refer you to ~mu init~). You can + avoid the addresses in the main screen and the warning by setting + ~mu4e-main-view-hide-addresses~ to non-nil. + + - In many cases, ~mu4e~ used to receive /all/ contacts after each indexing + operation; this was slow for some users, so we have updated this to /only/ + get the contacts that have changed since the last round. + + We also moved sorting the contacts to the mu-side, which speeds things up + further. However, as a side-effect of this, ~mu4e-contact-rewrite-function~ + and ~mu4e-compose-complete-ignore-address-regexp~ have been obsoleted; users + of those should migrate to ~mu4e-contact-process-function~; see its + docstring for details. + + - Christophe Troestler contributed support for Gnus' calender-invitation + handling in mu4e (i.e., you should be able to accept/reject invitations + etc.). It's very fresh code, and likely it'll be tweaked in the future. + But it's available now for testing. Note that this requires the gnus-based + viewer, as per ~(setq mu4e-view-use-gnus t)~ + + - In addition, he added support for custom headers, so the ones for for the + non-gnus-view should work just as well. + + - ~org-mode~ support is enabled by default now. ~speedbar~ support is disabled + by default. The support org functionality has been moved to ~mu4e-org.el~, + with ~org-mu4e.el~ remaining for older things. + + - ~mu4e~ now adds message-ids to messages when saving drafts, so we can find + them even with ~mu4e-headers-skip-duplicates~. + + - Bookmarks (as in ~mu4e-bookmarks~) are now simple plists (instead of cl + structs). ~make-mu4e-bookmark~ has been updated to produce such plists (for + backward compatibility). A bookmark now looks like a list of e.g. ~(:name + "My bookmark" :query "banana OR pear" :key ?f)~ this format is a bit easier + extensible. + + - ~mu4e~ recognizes an attribute ~:hide t~, which will hide the bookmark item + from the main-screen (and speedbar), but keep it available through the + completion UI. + + - ~mu4e-maildir-shortcuts~ have also become plists. The older format is still + recognized for backward compatibility, but you are encouraged to upgrade. + + - Replying to mailing-lists has been improved, allowing for choosing for + replying to all, sender, list-only. + + - A very visible change, ~mu4e~ now shows unread/all counts for bookmarks in + the main screen that are strings. This is on by default, but can be + disabled by setting ~:hide-unread~ in the bookmark ~plist~ to ~t~. For + speed-reasons, these counts do _not_ filter out duplicates nor messages that + have been removed from the filesystem. + + - ~mu4e-attachment-dir~ now also applies to composing messages; it determines + the default directory for inclusion. + + - The mu4e <-> mu interaction has been rewritten to communicate using + s-expressions, with a repl for testing. + +*** guile + + - guile 3.0 is now supported; guile 2.2 still works. + +*** toys + + - Updated the ~mug~ toy UI to use Webkit2/GTK+. Note that this is just a toy + which is not meant for distribution. ~msg2pdf~ is disabled for now. + + +*** How to upgrade mu4e + + - upgrade ~mu~ to the latest stable version (1.4.x) + + - shut down emacs + + - Run ~mu init~ in a terminal + + - Make sure ~mu init~ points to the right Maildir folder and add your email + address(es) the following way: + + ~mu init --maildir=~/Maildir --my-address=jim@example.com --my-address=bob@example.com~ + + - once this is done, run ~mu index~ + + - Don't forget to delete your old mail cache location if necessary (see + release notes for more detail). + +* Old news + :PROPERTIES: + :VISIBILITY: folded + :END: + +** 1.2 + + After a bit over a year since version 1.0, here is version 1.2. This is + mostly a bugfix release, but there are also a number of new features. + +*** mu + + - Substantial (algorithmic) speed-up of message-threading; this also (or + especially) affects mu4e, since threading is the default. See commit + eb9bfbb1ca3c for all the details, and thanks to Nicolas Avrutin. + + - The query-parser now generates better queries for wildcard searches, by + using the Xapian machinery for that (when available) rather than + transforming into regexp queries. + + - The perl backend is hardly used and will be removed; for now we just + disable it in the build. + + - Allow outputting messages in json format, closely following the sexp + output. This adds an (optional) dependency on the Json-Glib library. + +*** mu4e + + - Bump the minimal required emacs version to 24.4. This was already de-facto + true, now it is enforced. + + - In mu4e-bookmarks, allow the `:query` element to take a function (or + lambda) to dynamically generate the query string. + + - There is a new message-view for mu4e, based on the Gnus' article-view. + This bring a lot of (but not all) of the very rich Gnus article-mode + feature-set to mu4e, such as S/MIME-support, syntax-highlighting, + + For now this is experimental ("tech preview"), but might replace the + current message-view in a future release. Enable it with: + (setq mu4e-view-use-gnus t) + + Thanks to Christophe Troestler for his work on fixing various encoding + issues. + + - Many bug fixes + +*** guile + + - Now requires guile 2.2. + +*** Contributors for this release: + + Ævar Arnfjörð Bjarmason, Albert Krewinkel, Alberto Luaces, Alex Bennée, Alex + Branham, Alex Murray, Cheong Yiu Fung, Chris Nixon, Christian Egli, + Christophe Troestler, Dirk-Jan C. Binnema, Eric Danan, Evan Klitzke, Ian + Kelling, ibizaman, James P. Ascher, John Whitbeck, Junyeong Jeong, Kevin + Foley, Marcelo Henrique Cerri, Nicolas Avrutin, Oleh Krehel, Peter W. V. + Tran-Jørgensen, Piotr Oleskiewicz, Sebastian Miele, Ulrich Ölmann, + +** 1.0 + + After a decade of development, *mu 1.0*! + + Note: the new release requires a C++14 capable compiler. + +*** mu + + - New, custom query parser which replaces Xapian's 'QueryParser' + both in mu and mu4e. Existing queries should still work, but the new + engine handles non-alphanumeric queries much better. + - Support regular expressions in queries (with the new query engine), + e.g. "subject:/foo.*bar/". See the new `mu-query` and updated `mu-easy` + manpages for examples. + - cfind: ensure nicks are unique + - auxiliary programs invoked from mu/mu4e survive terminating the + shell / emacs + +*** mu4e + + - Allow for rewriting message bodies + - Toggle-menus for header settings + - electric-quote-(local-)mode work when composing emails + - Respect format=flowed and delsp=yes for viewing plain-text + messages + - Added new mu4e-split-view mode: single-window + - Add menu item for `untrash'. + - Unbreak abbrevs in mu4e-compose-mode + - Allow forwarding messages as attachments + (`mu4e-compose-forward-as-attachment') + - New defaults: default to 'skip duplicates' and 'include related' + in headers-view, which should be good defaults for most users. Can be + customized using `mu4e-headers-skip-duplicates' and + `mu4e-headers-include-related', respectively. + - Many bug fixed (see github for all the details). + - Updated documentation + +*** Contributors for this release: + + Ævar Arnfjörð Bjarmason, Alex Bennée, Arne Köhn, Christophe Troestler, + Damien Garaud, Dirk-Jan C. Binnema, galaunay, Hong Xu, Ian Kelling, John + Whitbeck, Josiah Schwab, Jun Hao, Krzysztof Jurewicz, maxime, Mekeor Melire, + Nathaniel Nicandro, Ronald Evers, Sean 'Shaleh' Perry, Sébastien Le + Callonnec, Stig Brautaset, Thierry Volpiatto, Titus von der Malsburg, + Vladimir Sedach, Wataru Ashihara, Yuri D'Elia. + + And all the people on the mailing-list and in github, with bug reports, + questions and suggestions. + + +** 0.9.18 + + New development series which will lead to 0.9.18. + +*** mu + + - Increase the default maximum size for messages to index to 500 + Mb; you can customize this using the --max-msg-size parameter to mu index. + - implement "lazy-checking", which makes mu not descend into + subdirectories when the directory-timestamp is up to date; greatly speeds + up indexing (see --lazy-check) + - prefer gpg2 for crypto + - fix a crash when running on OpenBSD + - fix --clear-links (broken filenames) + - You can now set the MU_HOME environment variable as an + alternative way of setting the mu homedir via the --muhome command-line + parameter. + +*** mu4e + +**** reading messages + + - Add `mu4e-action-view-with-xwidget`, and action for viewing + e-mails inside a Webkit-widget inside emacs (requires emacs 25.x with + xwidget/webkit/gtk3 support) + - Explicitly specify utf8 for external html viewing, so browsers + can handle it correctly. + - Make `shr' the default renderer for rich-text emails (when + available) + - Add a :user-agent field to the message-sexp (in mu4e-view), which + is either the User-Agent or X-Mailer field, when present. + +**** composing messages + + - Cleanly handle early exits from message composition as well as while + composing. + - Allow for resending existing messages, possibly editing them. M-x + mu4e-compose-resend, or use the menu; no shortcut. + - Better handle the closing of separate compose frames + - Improved font-locking for the compose buffers, and more extensive + checks for cited parts. + - automatically sign/encrypt replies to signed/encrypted messages + (subject to `mu4e-compose-crypto-reply-policy') + +**** searching & marking + + - Add a hook `mu4e-mark-execute-pre-hook`, which is run just before + executing marks. + - Just before executing any search, a hook-function + `mu4e-headers-search-hook` is invoked, which receives the search + expression as its parameter. + - In addition, there's a `mu4e-headers-search-bookmark-hook` which + gets called when searches get invoked as a bookmark (note that + `mu4e-headers-search-hook` will also be called just afterwards). This + hook also receives the search expression as its parameter. + - Remove the 'z' keybinding for leaving the headers + view. Keybindings are precious! + - Fix parentheses/precedence in narrowing search terms + +**** indexing + + - Allow for indexing in the background; see + `mu4e-index-update-in-background`. + - Better handle mbsync output in the update buffer + - Add variables mu4e-index-cleanup and mu4e-index-lazy to enable + lazy checking from mu4e; you can sit from mu4e using something like: +#+begin_src elisp +(setq mu4e-index-cleanup nil ;; don't do a full cleanup check + mu4e-index-lazy-check t) ;; don't consider up-to-date dirs #+END_SRC +#+end_src +**** misc + + - don't overwrite global-mode-string, append to it. + - Make org-links (and more general, all users of + mu4e-view-message-with-message-id) use a headers buffer, then view the + message. This way, those linked message are just like any other, and can + be deleted, moved etc. + - Support org-mode 9.x + - Improve file-name escaping, and make it support non-ascii filenames + - Attempt to jump to the same messages after a re-search update operation + - Add action for spam-filter options + - Let `mu4e~read-char-choice' become case-insensitive if there is + no exact match; small convenience that affects most the single-char + option-reading in mu4e. + +*** Perl + + - an experimental Perl binding ("mup") is available now. See + perl/README.md for details. + +*** Contributors: + + Aaron LI, Abdo Roig-Maranges, Ævar Arnfjörð Bjarmason, Alex Bennée, Allen, + Anders Johansson, Antoine Levitt, Arthur Lee, attila, Charles-H. Schulz, + Christophe Troestler, Chunyang Xu, Dirk-Jan C. Binnema, Jakub Sitnicki, + Josiah Schwab, jsrjenkins, Jun Hao, Klaus Holst, Lukas Fürmetz, Magnus + Therning, Maximilian Matthe, Nicolas Richard, Piotr Trojanek, Prashant + Sachdeva, Remco van 't Veer, Stephen Eglen, Stig Brautaset, Thierry + Volpiatto, Thomas Moulia, Titus von der Malsburg, Yuri D'Elia, Vladimir + Sedach + +** 0.9.16 + +*** Release + + 2016-01-20: Release from the 0.9.15 series + +*** Contributors: + + Adam Sampson, Ævar Arnfjörð Bjarmason, Bar Shirtcliff, Charles-H. Schulz, + Clément Pit--Claudel, Damien Cassou, Declan Qian, Dima Kogan, Dirk-Jan C. + Binnema, Foivos S. Zakkak, Hinrik Örn Sigurðsson, Jeroen Tiebout, JJ Asghar, + Jonas Bernoulli, Jun Hao, Martin Yrjölä, Maximilian Matthé, Piotr Trojanek, + prsarv, Thierry Volpiatto, Titus von der Malsburg + + (and of course all people who reported issues, provided suggestions etc.) + +** 0.9.15 + + - bump version to 0.9.15. From now on, odd minor version numbers + are for development versions; thus, 0.9.16 is to be the next stable + release. + - special case text/calendar attachments to get .vcs + extensions. This makes it easier to process those with external tools. + - change the message file names to better conform to the maildir + spec; this was confusing some tools. + - fix navigation when not running in split-view mode + - add `mu4e-view-body-face', so the body-face for message in the + view can be customized; e.g. (set-face-attribute 'mu4e-view-body-face nil + :font "Liberation Serif-10") + - add `mu4e-action-show-thread`, an action for the headers and view + buffers to search for messages in the same thread as the current one. + - allow for transforming mailing-list names for display, using + `mu4e-mailing-list-patterns'. + - some optimizations in indexing (~30% faster in some cases) + - new variable mu4e-user-agent-string, to customize the User-Agent: + header. + - when removing the "In-reply-to" header from replies, mu4e will + also remove the (hidden) References header, effectively creating a new + message-thread. + - implement 'mu4e-context', for defining and switching between + various contexts, which are groups of settings. This can be used for + instance for switch between e-mail accounts. See the section in the manual + for details. + - correctly decode mailing-list headers + - allow for "fancy" mark-characters; and improve the default set + - by default, the maildirs are no longer cached; please see the + variable ~mu4e-cache-maildir-list~ if you have a lot of maildirs and it + gets slow. + - change the default value for + ~org-mu4e-link-query-in-headers-mode~ to ~nil~, ie. by default link to the + message, not the query, as this is usually more useful behavior. + - overwrite target message files that already exist, rather than + erroring out. + - set mu4e-view-html-plaintext-ratio-heuristic to 5, as 10 was too + high to detect some effectively html-only messages + - add mu4e-view-toggle-html (keybinding: 'h') to toggle between + text and html display. The existing 'mu4e-view-toggle-hide-cited' gets the + new binding '#'. + - add a customization variable `mu4e-view-auto-mark-as-read' + (defaults to t); if set to nil, mu4e won't mark messages as read when you + open them. This can be useful on read-only file-systems, since + marking-as-read implies a file-move operation. + - use smaller chunks for mu server on Cygwin, allowing for better + mu4e support there. + +** 0.9.13 + +*** contributors + + Attila, Daniele Pizzolli, Charles-H.Schulz, David C Sterrat, Dirk-Jan C. + Binnema, Eike Kettner, Florian Lindner, Foivos S. Zakkak, Gour, KOMURA + Takaaki, Pan Jie, Phil Hagelberg, thdox, Tiago Saboga, Titus von der + Malsburg + + (and of course all people who reported issues, provided suggestions etc.) + +*** mu/mu4e/guile + + - NEWS (this file) is now visible from within mu4e – "N" in the main-menu. + + - make `mu4e-headers-sort-field', `mu4e-headers-sort-direction' + public (that, is change the prefix from mu4e~ to mu4e-), so users can + manipulate them + + - make it possible the 'fancy' (unicode) characters separately for + headers and marks (see the variable `mu4e-use-fancy-chars'.) + + - allow for composing in a separate frame (see + `mu4e-compose-in-new-frame') + + - add the `:thread-subject' header field, for showing the subject + for a thread only once. So, instead of (from the manual): + +#+begin_example +06:32 Nu To Edmund Dantès GstDev + Re: Gstreamer-V4L... +15:08 Nu Abbé Busoni GstDev + Re: Gstreamer-V... +18:20 Nu Pierre Morrel GstDev \ Re: Gstreamer... +2013-03-18 S Jacopo EmacsUsr + emacs server on win... +2013-03-18 S Mercédès EmacsUsr \ RE: emacs server ... +2013-03-18 S Beachamp EmacsUsr + Re: Copying a whole... +22:07 Nu Albert de Moncerf EmacsUsr \ Re: Copying a who... +2013-03-18 S Gaspard Caderousse GstDev | Issue with GESSimpl... +2013-03-18 Ss Baron Danglars GuileUsr | Guile-SDL 0.4.2 ava... +End of search results +#+end_example + +the headers list would now look something like: +#+begin_example +06:32 Nu To Edmund Dantès GstDev + Re: Gstreamer-V4L... +15:08 Nu Abbé Busoni GstDev + +18:20 Nu Pierre Morrel GstDev \ Re: Gstreamer... +2013-03-18 S Jacopo EmacsUsr + emacs server on win... +2013-03-18 S Mercédès EmacsUsr \ +2013-03-18 S Beachamp EmacsUsr + Re: Copying a whole... +22:07 Nu Albert de Moncerf EmacsUsr \ +2013-03-18 S Gaspard Caderousse GstDev | Issue with GESSimpl... +2013-03-18 Ss Baron Danglars GuileUsr | Guile-SDL 0.4.2 ava... +End of search results +#+end_example + + This is a feature known from e.g. `mutt' and `gnus` and many other + clients, and can be enabled by customizing `mu4e-headers-fields' + (replacing `:subject' with `:thread-subject') + + It's not the default yet, but may become so in the future. + + - add some spam-handling actions to mu4e-contrib.el + + - mu4e now targets org 8.x, which support for previous versions + relegated to `org-old-mu4e.el`. Some of the new org-features are improved + capture templates. + + - updates to the documentation, in particular about using BBDB. + + - improved URL-handling (use emacs built-in functionality) + + - many bug fixes, including some crash fixes on BSD + +*** guile + + – add --delete option to the find-dups scripts, to automatically delete + them. Use with care! + +** Release 0.9.12 + +*** mu + + - truncate /all/ terms the go beyond xapian's max term length + - lowercase the domain-part of email addresses in mu cfind (and mu4e), if + the domain is in ascii + - give messages without msgids fake-message-ids; this fixes the problem + where such messages were not found in --include-related queries + - cleanup of the query parser + - provide fake message-ids for messages without it; fixes #183 + - allow showing tags in 'mu find' output + - fix CSV quoting + +*** mu4e + + - update the emacs <-> backend protocol; documented in the mu-server man page + - show 'None' as date for messages without it (Headers View) + - add `mu4e-headers-found-hook', `mu4e-update-pre-hook'. + - split org support in org-old-mu4e.el (org <= 7.x) and org-mu4e.el + - org: improve template keywords + - rework URL handling + +** Release 0.9.5 + +*** mu + + - allow 'contact:' as a shortcut in queries for 'from:foo OR to:foo OR + cc:foo OR bcc:foo', and 'recip:' as a shortcut for 'to:foo OR cc:foo OR + bcc:foo' + - support getting related messages (--include-related), which includes + messages that may not match the query, but that are in the same threads as + messages that were + - support "list:"/"v:" for matching mailing list names, and the "v" + format-field to show them. E.g 'mu find list:emacs-orgmode.gnu.org' + +*** mu4e + + - scroll down in message view takes you to next message (but see + `mu4e-view-scroll-to-next') + - support 'human dates', that is, show the time for today's messages, and + the date for older messages in the headers view + - replace `mu4e-user-mail-address-regexp' and `mu4e-my-mail-addresses' with + `mu4e-user-mail-address-list' + - support tags (i.e.., X-Keywords and friends) in the headers-view, and the + message view. Thanks to Abdó Roig-Maranges. New field ":tags". + - automatically update the headers buffer when new messages are found during + indexing; set `mu4e-headers-auto-update' to nil to disable this. + - update mail/index with M-x mu4e-update-mail-and-index; which everywhere in + mu4e is available with key C-S-u. Use prefix argument to run in + background. + - add function `mu4e-update-index' to only update the index + - add 'friendly-names' for mailing lists, so they should up nicely in the + headers view + +*** guile + + - add 'mu script' command to run mu script, for example to do statistics on + your message corpus. See the mu-script man-page. + +*** mug + + - ported to gtk+ 3; remove gtk+ 2.x code + + + +** Release 0.9.9 <2012-10-14> + +*** mu4e + - view: address can be toggled long/short, compose message + - sanitize opening urls (mouse-1, and not too eager) + - tooltips for header labels, flags + - add sort buttons to header-labels + - support signing / decryption of messages + - improve address-autocompletion (e.g., ensure it's case-insensitive) + - much faster when there are many maildirs + - improved line wrapping + - better handle attached messages + - improved URL-matching + - improved messages to user (mu4e-(warn|error|message)) + - add refiling functionality + - support fancy non-ascii in the UI + - dynamic folders (i.e.., allow mu4e-(sent|draft|trash|refile)-folder) to + be a function + - dynamic attachment download folder (can be a function now) + - much improved manual + +*** mu + - remove --summary (use --summary-len instead) + - add --after for mu find, to limit to messages after T + - add new command `mu verify', to verify signatures + - fix iso-2022-jp decoding (and other 7-bit clean non-ascii) + - add support for X-keywords + - performance improvements for threaded display (~ 25% for 23K msgs) + - mu improved user-help (and the 'mu help' command) + - toys/mug2 replaces toys/mug + +*** mu-guile + - automated tests + - add mu:timestamp, mu:count + - handle db reopenings in the background + + +** Release 0.9.8.5 <2012-07-01> + +*** mu4e + + - auto-completion of e-mail addresses + - inline display of images (see `mu4e-view-show-images'), uses imagemagick + if available + - interactively change number of headers / columns for showing headers with + C-+ and C-- in headers, view mode + - support flagging message + - navigate to previous/next queries like a web browser (with , + ) + - narrow search results with '/' + - next/previous take a prefix arg now, to move to the nth previous/next message + - allow for writing rich-text messages with org-mode + - enable marking messages as Flagged + - custom marker functions (see manual) + - better "dwim" handling of buffer switching / killing + - deferred marking of message (i.e.., mark now, decide what to mark for + later) + - enable changing of sort order, display of threads + - clearer marks for marked messages + - fix sorting by subject (disregarding Re:, Fwd: etc.) + - much faster handling when there are many maildirs (speedbar) + - handle mailto: links + - improved, extended documentation + +*** mu + + - support .noupdate files (parallel to .noindex, dir is ignored unless we're + doing a --rebuild). + - append all inline text parts, when getting the text body + - respect custom maildir flags + - correctly handle the case where g_utf8_strdown (str) > len (str) + - make gtk, guile, webkit dependency optional, even if they are installed + + +** Release 0.9.8.4 <2012-05-08> + +*** mu4e + + - much faster header buffers + - split view mode (headers, view); see `mu4e-split-view'. + - add search history for queries + - ability to open attachments with arbitrary programs, pipe through shell + commands or open in the current emacs + - quote names in recipient addresses + - mu4e-get-maildirs works now for recursive maildirs as well + - define arbitrary operations for headers/messages/attachments using the + actions system -- see the chapter 'Actions' in the manual + - allow mu4e to be uses as the default emacs mailer (`mu4e-user-agent') + - mark headers based on a regexp, `mu4e-mark-matches', or '%' + - mark threads, sub-threads (mu4e-hdrs-mark-thread, + mu4e-hdrs-mark-subthread, or 'T', 't') + - add msg2pdf toy + - easy logging (using `mu4e-toggle-logging') + - improve mu4e-speedbar for use in headers/view + - use the message-mode FCC system for saving messages to the sent-messages + folder + - fix: off-by-one in number of matches shown + +*** general + + - fix for opening files with non-ascii names + - much improved support for searching non-Latin (Cyrillic etc.) languages + we can now match 'Тесла' or 'Аркона' without problems + - smarter escaping (fixes issues with finding message ids) + - fixes for queries with brackets + - allow --summary-len for the length of message summaries + - numerous other small fixes + + +** Release 0.9.8.3 <2012-04-06> + + *NOTE*: existing mu/mu4e are recommended to run `mu index --rebuild' after + installation. + +*** mu4e + + - allow for searching by editing bookmarks + (`mu4e-search-bookmark-edit-first') (keybinding 'B') + - make it configurable what to do with sent messages (see + `mu4e-sent-messages-behavior') + - speedbar support (initial patch by Antono V) + - better handling of drafts: + - don't save too early + - more descriptive buffer names (based on Subject, if any) + - don't put "--text-follows-this-line--" markers in files + - automatically include signatures, if set + - add user-settable variables mu4e-view-wrap-lines and mu4e-view-hide-cited, + which determine the initial way a message is displayed + - improved documentation + +*** general + + - much improved searching for GMail folders (i.e. maildir:/ matching); + this requires a 'mu index --rebuild' + - correctly handle utf-8 messages, even if they don't specify this explicitly + - fix compiler warnings for newer/older gcc and clang/clang++ + - fix unit tests (and some code) for Ubuntu 10.04 and FreeBSD9 + - fix warnings for compilation with GTK+ 3.2 and recent glib (g_set_error) + - fix mu_msg_move_to_maildir for top-level messages + - fix in maildir scanning + - plug some memleaks + +** Release 0.9.8.2 <2012-03-11> + +*** mu4e: + + - make mail updating non-blocking + - allow for automatic periodic update ('mu4e-update-interval') + - allow for external triggering of update + - make behavior when leaving the headers buffer customizable, ie. + ask/apply/ignore ('mu4e-headers-leave-behaviour') + +*** general + + - fix output for some non-UTF8 locales + - open ('play') file names with spaces + - don't show unnecessary errors for --format=links + - make build warning-free for clang/clang++ + - allow for slightly older autotools + - fix unit tests for some hidden assumptions (locale, dir structure etc.) + - some documentation updates / clarifications + +** Release 0.9.8.1 <2012-02-18 Sat> + +*** mu + - show only leaf/rfc822 MIME-parts + +*** mu4e + + - allow for shell commands with arguments in `mu4e-get-mail-command'. + - support marking messages as 'read' and 'unread' + - show the current query in the the mode-line (`global-mode-string'). + - don't repeat 'Re:' / 'Fwd:' + - colorize cited message parts + - better handling of text-based, embedded message attachments + - for text-bodies, concatenate all text/plain parts + - make filladapt dep optional + - documentation improvements + +** Release 0.9.8 <2012-01-31> + + - '--descending' has been renamed into '--reverse' + - search for attachment MIME-type using 'mime:' or 'y:' + - search for text in text-attachments using 'embed:' or 'e:' + - searching for attachment file names now uses 'file:' (was: 'attach:') + - experimental emacs-based mail client -- "mu4e" + - added more unit tests + - improved guile binding - no special binary is needed anymore, it's + installable are works with the normal guile system; code has been + substantially improved. still 'experimental' + +** Release 0.9.7 <2011-09-03 Sat> + + - don't enforce UTF-8 output, use locale (fixes issue #11) + - add mail threading to mu-find (using -t/--threads) (sorta fixes issue #13) + - add header line to --format=mutt-ab (mu cfind), (fixes issue #42) + - terminate mu view results with a form-feed marker (use --terminate) (fixes + issue #41) + - search X-Label: tags (fixes issue #40) + - added toys/muile, the mu guile shells, which allows for message stats etc. + - fix date handling (timezones) + +** Release 0.9.6 <2011-05-28 Sat> + + - FreeBSD build fix + - fix matching for mu cfind to be as expected + - fix mu-contacts for broken names/emails + - clear the contacts-cache too when doing a --rebuild + - wildcard searches ('*') for fields (except for path/maildir) + - search for attachment file names (with 'a:'/'attach:') -- also works with + wildcards + - remove --xquery completely; use --output=xquery instead + - fix progress info in 'mu index' + - display the references for a message using the 'r' character (xmu find) + - remove --summary-len/-k, instead use --summary for mu view and mu find, and + - support colorized output for some sub-commands (view, cfind and + extract). Disabled by default, use --color to enable, or set env MU_COLORS + to non-empty + - update documentation, added more examples + +** Release 0.9.5 <2011-04-25 Mon> + + - bug fix for infinite loop in Maildir detection + - minor fixes in tests, small optimizations + +** Release 0.9.4 <2011-04-12 Tue> + + - add the 'cfind' command, to search/export contact information + - add 'flag:unread' as a synonym for 'flag:new OR NOT flag:unseen' + - updated documentation + +** Release 0.9.3 <2011-02-13 Sun> + + - don't warn about missing files with --quiet + +** Release 0.9.2 <2011-02-02 Wed> + + - stricter checking of options; and options must now *follow* the sub-command + (if any); so, something like: 'mu index --maildir=/foo/bar' + - output searches as plain text (default), XML, JSON or s-expressions using + --format=plain|xml|json|sexp. For example: 'mu find foobar --output=json'. + These format options are experimental (except for 'plain') + - the --xquery option should now be used as --format=xquery, for output + symlinks, use --format=links. This is a change in the options. + - search output can include the message size using the 'z' shortcut + - match message size ranges (i.e.. size:500k..2M) + - fix: honor the --overwrite (or lack thereof) parameter + - support folder names with special characters (@, ' ', '.' and so on) + - better check for already-running mu index + - when --maildir= is not provided for mu index, default to the last one + - add --max-msg-size, to specify a new maximum message size + - move the 'mug' UI to toys/mug; no longer installable + - better support for Solaris builds, Gentoo. + +** Release 0.9.1 <2010-12-05 Sun> + + - Add missing icon for mug + - Fix unit tests (Issue #30) + - Fix Fedora 14 build (broken GTK+ 3) (Issue #31) + +** Release 0.9 <2010-12-04 Sat> + + - you can now search for the message priority ('prio:high', 'prio:low', + 'prio:normal') + - you can now search for message flags, e.g. 'flag:attach' for messages with + attachment, or 'flag:encrypted' for encrypted messages + - you can search for time-intervals, e.g. 'date:2010-11-26..2010-11-29' for + messages in that range. See the mu-find(1) and mu-easy(1) man-pages for + details and examples. + - you can store bookmarked queries in ~/.mu/bookmarks + - the 'flags' parameter has been renamed in 'flag' + - add a simple graphical UI for searching, called 'mug' + - fix --clearlinks for file systems without entry->d_type (fixes issue #28) + - make matching case-insensitive and accent-insensitive (accent-insensitive + for characters in Unicode Blocks 'Latin-1 Supplement' and 'Latin + Extended-A') + - more extensive pre-processing is done to make searching for email-addresses + and message-ids less likely to not work (issue #21) + - updated the man-pages + - experimental support for Fedora 14, which uses GMime 2.5.x (fixes issue #29) + +** Release 0.8 <2010-10-30 Sat> + + - There's now 'mu extract' for getting information about MIME-parts + (attachments) and extracting them + - Queries are now internally converted to lowercase; this solves some of the + false-negative issues + - All mu sub-commands now have their own man-page + - 'mu find' now takes a --summary-len= argument to print a summary of + up-to-n lines of the message + - Same for 'mu view'; the summary replaces the full body + - Setting the mu home dir now goes with -m, --muhome + - --log-stderr, --reindex, --rebuild, --autoupgrade, --nocleanup, --mode, + --linksdir, --clearlinks lost their single char version + +** Release 0.7 <2010-02-27 Sat> + + - Database format changed + - Automatic database scheme version check, notifies users when an upgrade + is needed + - 'mu view', to view mail message files + - Support for >10K matches + - Support for unattended upgrades - that is, the database can automatically + by upgraded (--autoupgrade). Also, the log file is automatically cleaned + when it gets too big (unless you use --nocleanup) + - Search for a certain Maildir using the maildir:,m: search prefixes. For + example, you can find all messages located in ~/Maildir/foo/bar/cur/msg + ~/Maildir/foo/bar/new/msg and with m:/foo/bar this replace the search for + path/p in 0.6 + - Fixes for reported issues () + - A test suite with a growing number of unit tests + + +** Release 0.6 <2010-01-23 Sat> + + - First new release of mu since 2008 + - No longer depends on sqlite + + +# Local Variables: +# mode: org; org-startup-folded: nil +# fill-column:80 +# End: diff --git a/README.org b/README.org new file mode 100644 index 0000000..73426be --- /dev/null +++ b/README.org @@ -0,0 +1,96 @@ +#+TITLE:mu +[[https://github.com/djcb/mu/blob/master/COPYING][https://img.shields.io/github/license/djcb/mu?logo=gnu&.svg]] +[[https://en.cppreference.com][https://img.shields.io/badge/Made%20with-C/CPP-1f425f?logo=c&.svg]] +[[https://img.shields.io/github/v/release/djcb/mu][https://img.shields.io/github/v/release/djcb/mu.svg]] +[[https://github.com/djcb/mu/graphs/contributors][https://img.shields.io/github/contributors/djcb/mu.svg]] +[[https://github.com/djcb/mu/issues][https://img.shields.io/github/issues/djcb/mu.svg]] +[[https://github.com/djcb/mu/issues?q=is%3Aissue+is%3Aopen+label%3Arfe][https://img.shields.io/github/issues/djcb/mu/rfe?color=008b8b.svg]] +[[https://github.com/djcb/mu/pull/new][https://img.shields.io/badge/PRs-welcome-brightgreen.svg]]\\ +[[https://melpa.org/#/?q=mu4e&sort=version&asc=false][https://img.shields.io/badge/Emacs-25.3-922793?logo=gnu-emacs&logoColor=b39ddb&.svg]] +[[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Dependencies-for-Debian_002fUbuntu][https://img.shields.io/badge/Platform-Linux-2e8b57?logo=linux&.svg]] +[[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Building-from-a-release-tarball-1][https://img.shields.io/badge/Platform-FreeBSD-8b3a3a?logo=freebsd&logoColor=c32136&.svg]] +[[https://formulae.brew.sh/formula/mu#default][https://img.shields.io/badge/Platform-macOS-101010?logo=apple&logoColor=ffffff&.svg]] + +Welcome to ~mu~! + +*Note*: you are looking at the *development* branch, which is where new code is +being developed and tested, and which may occasionally break. + +Distribution and non-adventurous users are instead recommended to use the [[https://github.com/djcb/mu/tree/release/1.8][1.8 +Release Branch]] or to pick up one of the [[https://github.com/djcb/mu/releases][1.8 Releases]]. + +Given the enormous amounts of e-mail many people gather and the importance of +e-mail message in our work-flows, it's essential to quickly deal with all that +mail - in particular, to instantly find that one important e-mail you need right +now, and quickly file away message for later use. + +~mu~ is a tool for dealing with e-mail messages stored in the Maildir-format. ~mu~'s +purpose in life is to help you to quickly find the messages you need; in +addition, it allows you to view messages, extract attachments, create new +maildirs, and so on. =mu= is fully documented. + +After indexing your messages into a [[http://www.xapian.org][Xapian]]-database, you can search them using a +custom query language. You can use various message fields or words in the body +text to find the right messages. + +Built on top of ~mu~ are some extensions (included in this package): + +- ~mu4e~: a full-featured e-mail client that runs inside emacs + +- ~mu-guile~: bindings for the Guile/Scheme programming language (version 3.0 and + later) + +~mu~ is written in C and C++; ~mu4e~ is written in ~elisp~ and ~mu-guile~ in a mix of C++ and +Scheme. + +Note, ~mu~ is available in Linux distributions (e.g. Debian/Ubuntu and Fedora) +under the name ~maildir-utils~; apparently because they don't like short names. +All of the code is distributed under the terms of the [[https://www.gnu.org/licenses/gpl-3.0.en.html][GNU General Public License +version 3]] (or higher). + +* Installation + +Note: building from source is an /advanced/ subject; esp. if something goes +wrong. The below simple examples are a start, but all tools involved have many +options; there are differences between systems, versions etc. So if this is all +a bit daunting we recommend to wait for someone else to build it for you, such +as a Linux distribution. Many have packages available. + +** Requirements + +To be able to build ~mu~, ensure you have: + +- a C++17 compiler (~gcc~ or ~clang~ are known to work) +- development packages for /Xapian/ and /GMime/ and /GLib/ (see ~meson.build~ for the + versions) +- basic tools such as ~make~, ~sed~, ~grep~ +- ~meson~ + +For ~mu4e~, you also need ~emacs~. + +** Building + +#+begin_example +$ git clone git://github.com/djcb/mu.git +$ cd mu +#+end_example + +Now, you have a choice. ~mu~ uses ~meson~ for building, but includes a good-old +~Makefile~ with some useful targets, which should work for typical cases. + +#+begin_example +$ ./autogen.sh && make +$ sudo make install +#+end_example + +Alternatively, for more control, you can run ~meson~ directly: +#+begin_example +$ meson build && ninja -C build +$ ninja -C build install +#+end_example + +This allows for passing various ~meson~ options, such as ~--prefix~. Consult the +~meson~ documentation for details. + + + diff --git a/TODO b/TODO new file mode 100644 index 0000000..f152855 --- /dev/null +++ b/TODO @@ -0,0 +1,159 @@ +#+STARTUP: showall + +* TODO (fixes, ideas, etc.) + +** Future stuff + +*** mu + + - put threading information in the database, and enable getting the complete + threads when searching + - refactor fill_database function in test cases + - don't show duplicate e-mails (i.e.. for Gmail); check the message-id + +*** mu-guile + + - move contact export to separate scm + - fix logging + +*** mu4e + + - special-case replying to messages sent by self + - identities (see Jacek's 'mu4e: From field in replies' mail) + ==> [ workaround available, using mu4e-pre-compose-hook, dynamic folders ] + - new-mail warning + ==> [ workaround available, using mu4e-index-updated-hook ] + - custom header fields in headers-view, message-view + - show maildirs as a tree, not a list in speed bar + - review emacs menus + - re-factor / separate window/buffer management + - enable keeping message view buffers around + - better naming for draft/view buffers + - header updating interferes with marks (when updating for 'mark as read', + when reading a marked message) + - set/unset flag editing command + - handling of database upgrades + - restore point after rerunning a search + - make the mu4e-bookmarks format similar to the other ones + - refresh current query after update? + - fix mu4e-mark-set to work from the view buffer as well + - open links to mails through headers-mode somehow (i.e.., + mu4e-view-message-with-msgid) + - improve mouse interaction (i.e., cursor vs point) + - show counts of messages in searches (in main view) + - show flush only if there's something to flush (and # of flushables) + - fix unsafe temp-file handling + - make copy paste name/address in mu4e-view possible + + +* Done (0.9.9.x) + + - mu4e: scroll down –> go to next message + - mu: add contact: as a shortcut for matching from/to/cc/bcc: + - guile integration + - statistics + - 'human' dates in the headers view + - :tags in headers, message view + +* Done + :PROPERTIES: + :VISIBILITY: folded + :END: + + +** Done (0.9.9) + + - make contacts in the view clickable (toggle long/short display, compose message) + - opening urls is too eager (now use M-RET for opening url at point, not just + RET, which conflicted with using RET for scrolling) + - document quoting of queries + - use mu-error + - tooltips in header labels + - tooltip for flags field + - remove --summary option (for mu find, mu view); use --summary-len instead + - add sort buttons to header labels (and do the sorting) + - cleanup mu-cmd-find + - implement --after for mu find, to only show message files changed after a + certain time (mtime) + - add mu:timestamp for guile (referring to the message file's mtime) + - guile automated tests + - add 'mu verify' + - automated tests + - handle verbose/quiet/normal output 'mu verify' + - check gmime 2.4 does not break + - hook up mu4e with 'mu verify' + - add 'help' command + - refactor mu-msg-part + - move widgets/ into toys/mug2, remove toys/mug/, rename toys/mug2 -> toys/mug + - add guile mu:count + - don't show GPG/PKCS7 sigs as attachments + - fix address completion (quote names) + - add support for X-Keywords (in addition to X-Label) + - guile: add stats test cases + - fixed iso-2022-jp (japanese) decoding + - make address completion case-insensitive + - recognize '*' in urls + - handle exception 'The revision being read has been discarded - you should + call Xapian::Database::reopen() and retry the operation' + - handle passwords from get-mail shell command + - support fancy (non-ascii) chars for header flags, thread prefix strings + - improve performance of getting the list of maildirs + - fix setting wrapped/hide state in viewer + - fix ' realpath() failed for...' stuff + - allow for fancy chars (> ascii), make it configurable (mu4e-use-fancy-chars) + - don't user `error' for user-errors + - better echo-area reporting + - improve help feedback for user (command line) + - handling of encrypted messages + - improved checked for gmime-2.6 crypto funcs + - handling of command line options / help + - fix / add support for :size + - mu4e~view-wrap-lines (use visual-line-mode? see Jacek's mu4e~view-wrap-lines + mail) + - better help + - threading optimizations + - actions for /all/ headers, actions for /all/ attachment + - handle attached messages with attachments + +** Done (0.8.9.5) + + - make next/prev header respect prefix argument (Jacek's patch) + - make search results a stack (well, multiple stacks) + - optionally keep cc with user's email + - enable setting/unsetting 'Flagged' on messages + - allow narrowing of search results + - interactive split-view control (Jacek) + - view images inline + - *FIX* slow maildirs when there are many + - *FIX* ignore unrecognized maildir flag letters + - *FIX*: reply-to does not make it to the frontend + - *FIX* wrong buffer deleted after sending (see '(non mu) buffer is killed') + - rich text composing (with org-mode) + - let message-mode deal with burying/killing compose buffers + - *FIX* add runtime check for imagemagick + - *FIX* no error note if target message already exists (when moving) + - sorting + show / hide threads + - *FIX* having multiple header views visible + - *FIX* fix for strings where len (g_utf8_strdown (str)) > len (str) + - make sure marks correspond to the *current* message in message view (see + https://github.com/djcb/mu/issues/26) + - *FIX* don't remove unknown message flags when moving + - make guile/gtk/webkit dependency optional + - improve fringe marks (see https://github.com/djcb/mu/issues/21) + - mark message, decide what to do with them later (i.e.. 'deferred marking') + - custom predicate functions for marking + - make mu4e buffer killing less aggressive (i.e.., DWIM) + - about mu4e + - hide some headers when composing + - fix sorting subjects with ':' (but not 'Re:' or 'Fwd:') + - strip signature from original when replying + - make refresh after changing sort, threads the default + - contact completion (see Jacek's 'mu4e: using' mail) + - *FIX* emacs23 mailto: handling + - *FIX* message interference + - *FIX* emacs23.2+ auto-completion + + +# Local Variables: +# mode: org +# End: diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..eac4d6b --- /dev/null +++ b/autogen.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Run this to generate all the initial makefiles, etc. + +echo "*** meson build setup" + +test -f mu/mu.cc || { + echo "*** Run this script from the top-level mu source directory" + exit 1 +} + +BUILDDIR=build + +command -v meson 2> /dev/null +if [ $? != 0 ]; then + echo "*** No meson found, please install it ***" + exit 1 +fi + +# we could remove build/ but let's avoid rm -rf risks... +if test -d ${BUILDDIR}; then + meson --reconfigure ${BUILDDIR} $@ +else + meson ${BUILDDIR} $@ +fi + +# Add a Makefile with some useful target +cp Makefile.meson Makefile + +echo "*** Now run 'ninja -C ${BUILDDIR}' to build mu" +echo "*** Or check the Makefile for some useful targets" diff --git a/build-aux/config.rpath b/build-aux/config.rpath new file mode 100644 index 0000000..e69de29 diff --git a/build-aux/meson-install-info.sh b/build-aux/meson-install-info.sh new file mode 100644 index 0000000..9efe63d --- /dev/null +++ b/build-aux/meson-install-info.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +infodir=$1 +infofile=$2 + +# Meson post-install script to update info metadata +# If DESTDIR is set, do _not_ install-info, since it's only a temporary +# install +if test -z "${DESTDIR}"; then + install-info --info-dir ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir} \ + ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir}/${infofile} +fi + +gzip --force ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir}/${infofile} diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..5e44d5d --- /dev/null +++ b/configure.ac @@ -0,0 +1,320 @@ +## Copyright (C) 2008-2023 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +AC_PREREQ([2.68]) +AC_INIT([mu],[1.8.13],[https://github.com/djcb/mu/issues],[mu]) +AC_COPYRIGHT([Copyright (C) 2008-2022 Dirk-Jan C. Binnema]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_SRCDIR([mu/mu.cc]) +# libtoolize wants to put some stuff in here; if you have an old +# autotools/libtool setup. you can try to comment this out +AC_CONFIG_MACRO_DIR([m4]) +AC_CONFIG_AUX_DIR([build-aux]) + +AM_INIT_AUTOMAKE([1.14 foreign no-dist-gzip tar-ustar dist-xz]) + +# silent build if we have a new enough automake +m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) + +AS_IF([test x$prefix = xNONE],[prefix=/usr/local]) +AC_SUBST(prefix) + +# AC_PROG_CXX *before* AC_PROG_CC, otherwise configure won't error out +# when a c++ compiler is not found. Weird, huh? +AC_PROG_CXX +AC_PROG_CC +AC_PROG_CPP +AC_PROG_INSTALL +AC_CHECK_INCLUDES_DEFAULT +AC_PROG_EGREP + + +extra_flags="-Wformat-security \ + -Wstack-protector \ + -Wstack-protector-all \ + -Wno-cast-function-type \ + -Wno-bad-function-cast \ + -Wno-switch-enum" + +AX_CXX_COMPILE_STDCXX_17 +AX_COMPILER_FLAGS_CXXFLAGS([],[],[${extra_cflags}]) +AX_APPEND_COMPILE_FLAGS([-Wno-inline],[CXXFLAGS]) +AX_VALGRIND_CHECK + +LT_INIT + +AX_CODE_COVERAGE + +AC_PROG_AWK +AC_CHECK_PROG(SORT,sort,sort) + +AC_CHECK_HEADERS([wordexp.h]) + +# use the 64-bit versions +AC_SYS_LARGEFILE + +# asan is somewhat similar to valgrind, but has low enough overhead so it +# can be used during normal operation. +AC_ARG_ENABLE([asan],[AS_HELP_STRING([--enable-asan], + [Enable Address Sanitizer])], [use_asan=$enableval], [use_asan=no]) +AS_IF([test "x$use_asan" = "xyes"],[ + AC_SUBST(ASAN_CFLAGS, "-fsanitize=address -static-libasan -fno-omit-frame-pointer") + AC_SUBST(ASAN_CXXFLAGS,"-fsanitize=address -static-libasan -fno-omit-frame-pointer") + AC_SUBST(ASAN_LDFLAGS, "-fsanitize=address -static-libasan -fno-omit-frame-pointer") +]) + + +# check for makeinfo +AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no) +AM_CONDITIONAL(HAVE_MAKEINFO,test "x$have_makeinfo" = "xyes") +AM_COND_IF(HAVE_MAKEINFO,[],[ + # seems build *insists* on trying to makeinfo, erroring out + # if it does not exist. Let's work around that. + AC_SUBST(MAKEINFO,[true]) +]) + +# we need emacs for byte-compiling mu4e +build_mu4e=no +AC_ARG_ENABLE([mu4e], + AS_HELP_STRING([--disable-mu4e],[Disable building mu4e])) +AS_IF([test "x$enable_mu4e" != "xno"], [ + AM_PATH_LISPDIR + AS_IF([test "x$lispdir" != "xno"], [ + emacs_version="$($EMACS --version | head -1)" + lispdir="${lispdir}/mu4e/" + ]) + AS_CASE([$emacs_version], + [*25.3*],[build_mu4e=yes], + [*26*|*27*|*28*|*29*],[build_mu4e=yes], + [AC_MSG_WARN(emacs is too old to build mu4e (need emacs >= 25.3))]) +]) +AM_CONDITIONAL(BUILD_MU4E, test "x$build_mu4e" = "xyes") + +# we need some special tricks for filesystems that don't have d_type; +# e.g. Solaris. See mu-maildir.c. Explicitly disabling it is for +# testing purposes only +AC_ARG_ENABLE([dirent-d-type], + AS_HELP_STRING([--disable-dirent-d-type],[Don't use dirent->d_type, even if you have it]), + [], [AC_STRUCT_DIRENT_D_TYPE] +) +AS_IF([test "x$ac_cv_member_struct_dirent_d_type" != "xyes"], + [use_dirent_d_type="no"], [use_dirent_d_type="yes"]) + +# support for d_ino (inode) in struct dirent is optional; if it's +# available we can sort direntries by inode and access them in that +# order; this is much faster on some file systems (such as extfs3). +# Explicitly disabling it is for testing purposes only. +AC_ARG_ENABLE([dirent-d-ino], + AS_HELP_STRING([--disable-dirent-d-ino],[Don't use dirent->d_ino, even if you have it]), + [], [AC_STRUCT_DIRENT_D_INO] +) +AS_IF([test "x$ac_cv_member_struct_dirent_d_ino" != "xyes"], + [use_dirent_d_ino="no"], [use_dirent_d_ino="yes"]) + +AC_CHECK_FUNCS([memset memcpy realpath setlocale strerror getpass setsid]) +AC_CHECK_FUNCS([vasprintf strptime]) +# timegm is no longer used in the source +# AC_CHECK_FUNC(timegm,[],AC_MSG_ERROR([missing required function timegm])) + +# require pkg-config >= 0.28 (release in 2013; should be old enough...) +# with that version, we don't need the AC_SUBST stuff after PKG_CHECK. +m4_ifndef([PKG_PROG_PKG_CONFIG], + [m4_fatal([please install pkg-config >= 0.28 before running autoconf/autogen])]) +PKG_PROG_PKG_CONFIG(0.28) # latest version in buildroot +AS_IF([test -z "$PKG_CONFIG"], + AC_MSG_ERROR([ + *** pkg-config with version >= 0.28 could not be found. + *** + *** Make sure it is in your path, or set the PKG_CONFIG environment variable + *** to the full path to pkg-config.]) +) + +# glib2? +PKG_CHECK_MODULES(GLIB,glib-2.0 >= 2.58 gobject-2.0 gio-2.0) +glib_version="$($PKG_CONFIG --modversion glib-2.0)" + +# gmime, version 3.0 or higher +PKG_CHECK_MODULES(GMIME,gmime-3.0) +gmime_version="$($PKG_CONFIG --modversion gmime-3.0)" + +# xapian checking - we need 1.4 at least +PKG_CHECK_MODULES(XAPIAN,xapian-core >= 1.4,[ + have_xapian=yes + xapian_version=$($PKG_CONFIG xapian-core --modversion) + AC_SUBST(XAPIAN_CXXFLAGS,${XAPIAN_CFLAGS}) +],[ + # fall back to the xapian-config script. Not sure if there are cases where the + # pkgconfig does not work, but xapian-config does, so keep this for now. + AC_MSG_NOTICE([falling back to xapian-config]) + AC_CHECK_PROG(XAPIAN_CONFIG,xapian-config,xapian-config,no) + AS_IF([test "x$XAPIAN_CONFIG" = "xno"],[ + AC_MSG_ERROR([ + *** xapian could not be found; please install it + *** e.g., in debian/ubuntu the package would be 'libxapian-dev' + *** If you compiled it yourself, you should ensure that xapian-config + *** is in your PATH.])], + [xapian_version=$($XAPIAN_CONFIG --version | sed -e 's/.* //')]) + + AS_CASE([$xapian_version], + [1.[[4-9]].[[0-9]]*], + [AC_MSG_NOTICE([xapian $xapian_version found.])], + [AC_MSG_ERROR([*** xapian version >= 1.4 needed, but version $xapian_version found.])]) + + XAPIAN_CXXFLAGS="$($XAPIAN_CONFIG --cxxflags)" + XAPIAN_LIBS="$($XAPIAN_CONFIG --libs)" + have_xapian="yes" + + AC_SUBST(XAPIAN_CXXFLAGS) + AC_SUBST(XAPIAN_LIBS) +]) +############################################################################### +# we set the set the version of the MuStore (Xapian database) layout +# here; it will become part of the db name, so we can automatically +# recreate the database when we have incompatible changes. +# +# note that MU_STORE_SCHEMA_VERSION does not follow mu versioning, as we +# hopefully don't have updates for each version; also, this has nothing to do +# with Xapian's software versionx +AC_DEFINE(MU_STORE_SCHEMA_VERSION,["465"],['Schema' version of the database]) +############################################################################### + +################################################################################ +# should we try to build an emacs dynamic module? +#AC_CHECK_HEADER([emacs-module.h],[ +# AC_DEFINE([HAVE_EMACS_MODULE_H],[1], [Whether we have the emacs-module header])], +# AC_MSG_NOTICE([emacs-module.h not found; not building module]) +#) +#AM_CONDITIONAL([BUILD_EMACS_MODULE],[test "x$ac_cv_header_emacs_module_h" != "x"]) +################################################################################ + +############################################################################### +# build with guile 3.0/2.2 when available and not disabled. +AC_ARG_ENABLE([guile], AS_HELP_STRING([--disable-guile],[Disable guile])) +AS_IF([test "x$enable_guile" != "xno"],[ + + PKG_CHECK_MODULES(GUILE, [guile-3.0], [have_guile=yes],[ + PKG_CHECK_MODULES(GUILE, [guile-2.2], [have_guile=yes], [have_guile=no])]) + AS_IF([test "x$have_guile" = "xyes"],[ + GUILE_PKG([3.0 2.2]) + GUILE_PROGS + GUILE_FLAGS + AC_DEFINE_UNQUOTED([GUILE_BINARY],"$GUILE",[guile binary]) + vsnarf=guile-snarf${GUILE_EFFECTIVE_VERSION} + AC_CHECK_PROGS(GUILE_SNARF,[${vsnarf} guile-snarf], [no]) + guile_version=$($PKG_CONFIG guile-$GUILE_EFFECTIVE_VERSION --modversion) + ]) +]) + +AM_CONDITIONAL(BUILD_GUILE,[test "x$have_guile" = "xyes" -a \ + "x$ac_cv_prog_GUILE_SNARF" != "xno"]) +AM_COND_IF([BUILD_GUILE],[AC_DEFINE(BUILD_GUILE,[1], [Do we support Guile?])]) +############################################################################### + +############################################################################### +# optional readline +AC_ARG_ENABLE([readline], AS_HELP_STRING([--disable-readline],[Disable readline])) +AS_IF([test "x$enable_readline" != "xno"], [ + saved_libs=$LIBS + AX_LIB_READLINE + AC_SUBST(READLINE_LIBS,${LIBS}) + LIBS=$saved_libs +]) +############################################################################### + +############################################################################### +# check for makeinfo +AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no) +AM_CONDITIONAL(HAVE_MAKEINFO, [test "x$have_makeinfo" = "xyes"]) +############################################################################### + +############################################################################### +# docdir, so we can use it in mu4e-meta.el.in +AC_SUBST(MU_DOC_DIR, "${prefix}/share/doc/mu") +############################################################################### + +AC_CONFIG_FILES([ +Makefile +mu/Makefile +lib/Makefile +lib/doxyfile +lib/thirdparty/Makefile +lib/utils/Makefile +lib/message/Makefile +lib/index/Makefile +mu4e/Makefile +mu4e/mu4e-config.el +guile/Makefile +guile/mu/Makefile +guile/examples/Makefile +guile/scripts/Makefile +man/Makefile +m4/Makefile +contrib/Makefile +]) +AC_CONFIG_FILES([mu/mu-memcheck], [chmod +x mu/mu-memcheck]) + +AC_OUTPUT + +dnl toys/msg2pdf/Makefile + +echo +echo "mu configuration is complete." +echo "------------------------------------------------" + +echo "mu version : $VERSION" +echo +echo "Xapian version : $xapian_version" +echo "GLib version : $glib_version" +echo "GMime version : $gmime_version" + +AM_COND_IF([BUILD_GUILE],[ +echo "Guile version : $guile_version" +]) +echo +echo "Have direntry->d_ino : $use_dirent_d_ino" +echo "Have direntry->d_type : $use_dirent_d_type" +echo "------------------------------------------------" +echo + +# +# Warnings / notes +# + +# makeinfo +if test "x$have_makeinfo" != "xyes"; then + echo "* You do not seem to have the makeinfo program; if you are building from git" + echo " you need that to create documentation for guile and emacs. It is in the" + echo " texinfo package in debian/ubuntu/fedora/... " + echo +fi + +# wordexp +AS_IF([test "x$ac_cv_header_wordexp_h" != "xyes"],[ + echo "* Your system does not seem to have the 'wordexp' function." + echo " This means that you cannot use shell-like expansion in options and " + echo " some other places. So, for example, instead of" + echo " --maildir=~/Maildir" + echo " you should use the complete path, something like:" + echo " --maildir=/home/user/Maildir" +]) + + +echo "NOTE: autotools support has been deprecated and will be removed" +echo " after the next stable release. use the meson build instead" + +echo +echo "Now, type 'make' (or 'gmake') to build mu" +echo diff --git a/contrib/Makefile.am b/contrib/Makefile.am new file mode 100644 index 0000000..00687a6 --- /dev/null +++ b/contrib/Makefile.am @@ -0,0 +1,26 @@ +## Copyright (C) 2008-2013 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CPPFLAGS=$(GMIME_CFLAGS) $(GLIB_CFLAGS) -I${prefix}/include +AM_CFLAGS=-Wall -Wextra -Wno-unused-parameter -Wdeclaration-after-statement -pedantic + + +EXTRA_DIST= \ + mu-completion.zsh \ + mu-sexp-convert \ + mu.spec diff --git a/contrib/mu-completion.zsh b/contrib/mu-completion.zsh new file mode 100644 index 0000000..ea2bdbd --- /dev/null +++ b/contrib/mu-completion.zsh @@ -0,0 +1,124 @@ +#compdef mu + +## Copyright (C) 2011-2012 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# zsh completion for mu. Install this by copying/linking to this file somewhere in +# your $fpath; the link/copy must have a name starting with an underscore "_" + +# main dispatcher function +_mu() { + if (( CURRENT > 2 )) ; then + local cmd=${words[2]} + curcontext="${curcontext%:*:*}:mu-$cmd" + (( CURRENT-- )) + shift words + _call_function ret _mu_$cmd + return ret + else + _mu_commands + fi +} + + + +_mu_commands() { + local -a mu_commands + mu_commands=( + 'index:scan your maildirs and import their metadata in the database' + 'find:search for messages in the database' + 'view:display specific messages' + 'cfind:search for contacts (name + email) in the database' + 'extract:extract message-parts (attachments) and save or open them' + 'mkdir:create maildirs' +# below are not generally very useful, so let's not auto-complete them +# 'add: add a message to the database.' +# 'remove:remove a message from the database.' +# 'server:sart the mu server' +) + + _describe -t command 'command' mu_commands +} + +_mu_common_options=( + '--debug[output information useful for debugging mu]' + '--quiet[do not give any non-critical information]' + '--nocolor[do not use colors in some of the output]' + '--version[display mu version and copyright information]' + '--log-stderr[log to standard error]' +) + +_mu_db_options=( + '--muhome[use some non-default location for the mu database]:directory:_files' +) + +_mu_find_options=( + '--fields[fields to display in the output]' + '--sortfield[field to sort the output by]' + '--descending[sort in descending order]' + '--summary[include a summary of the message]' + '--summary-len[number of lines to use for the summary]' + '--bookmark[use a named bookmark]' + '--output[set the kind of output for the query]' +) + +_mu_view_options=( + '--summary[only show a summary of the message]' + '--summary-len[number of lines to use for the summary]' +) + + +_mu_view() { + _arguments -s : \ + $_mu_common_options \ + $_mu_view_options +} + +_mu_extract() { + _files +} + +_mu_find() { + _arguments -s : \ + $_mu_common_options \ + $_mu_db_options \ + $_mu_find_options +} + +_mu_index() { + _arguments -s : \ + $_mu_db_options \ + $_mu_common_options +}mu + +_mu_cleanup() { + _arguments -s : \ + $_mu_db_options \ + $_mu_common_options +} + + +_mu_mkdir() { + _arguments -s : \ + '--mode=[file mode for the new Maildir]:file mode: ' \ + $_mu_common_options +} + +_mu "$@" + +# Local variables: +# mode: sh +# End: diff --git a/contrib/mu-sexp-convert b/contrib/mu-sexp-convert new file mode 100755 index 0000000..b2835ac --- /dev/null +++ b/contrib/mu-sexp-convert @@ -0,0 +1,204 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; Copyright (C) 2012 Dirk-Jan C. Binnema +;; +;; 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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; +;; a little hack to convert the output of +;; mu find --format=sexp +;; and +;; mu view --format=sexp +;; into XML or JSON + +(use-modules (ice-9 getopt-long) (ice-9 format) (ice-9 regex)) +(use-modules (sxml simple)) + +(define (mapconcat func lst sepa) + "Apply FUNC to elements of LST, concat the result as strings +separated by SEPA." + (if (null? lst) + "" + (string-append + (func (car lst)) + (if (null? (cdr lst)) + "" + (string-append sepa (mapconcat func (cdr lst) sepa)))))) + +(define (property-list? obj) + "Is OBJ a elisp-style property list (ie. a list of the +form (:symbol1 something :symbol2 somethingelse), as in an elisp +proplilst." + (and (list? obj) + (not (null? obj)) + (symbol? (car obj)) + (string= ":" (substring (symbol->string (car obj)) 0 1)))) + +(define (plist->pairs plist) + "Convert an elisp-style property list; e.g: + (:prop1 foo :prop2: bar ...) +into a list of pairs + ((prop1 . foo) (prop2 . bar) ...)." + (if (null? plist) + '() + (cons + (cons + (substring (symbol->string (car plist)) 1) + (cadr plist)) + (plist->pairs (cddr plist))))) + +(define (string->xml str) + "XML-encode STR." + ;; sneakily re-using sxml->xml + (call-with-output-string (lambda (port) (sxml->xml str port)))) + +(define (string->json str) + "Convert string into a JSON-encoded string." + (letrec ((convert + (lambda (lst) + (if (null? lst) + "" + (string-append + (cond + ((equal? (car lst) #\") "\\\"") + ((equal? (car lst) #\\) "\\\\") + ((equal? (car lst) #\/) "\\/") + ((equal? (car lst) #\bs) "\\b") + ((equal? (car lst) #\ff) "\\f") + ((equal? (car lst) #\lf) "\\n") + ((equal? (car lst) #\cr) "\\r") + ((equal? (car lst) #\ht) "\\t") + (#t (string (car lst)))) + (convert (cdr lst))))))) + (convert (string->list str)))) + +(define (etime->time_t t) + "Convert elisp time object T into a time_t value." + (logior (ash (car t) 16) (car (cdr t)))) + +(define (sexp->xml) + "Convert string INPUT to XML, return the XML (string)." + (letrec ((convert-xml + (lambda* (expr #:optional parent) + (cond + ((property-list? expr) + (mapconcat + (lambda (pair) + (format #f "\t<~a>~a\n" + (car pair) (convert-xml (cdr pair) (car pair)) (car pair))) + (plist->pairs expr) " ")) + ((list? expr) + (cond + ((member parent '("from" "to" "cc" "bcc")) + (mapconcat (lambda (addr) + (format #f "
~a~a
" + (if (string? (car addr)) + (format #f "~a" + (string->xml (car addr))) "") + (if (string? (cdr addr)) + (format #f "~a" + (string->xml (cdr addr))) ""))) + expr " ")) + ((string= parent "parts") "") ;; for now, ignore + ;; convert the crazy emacs time thingy to time_t... + ((string= parent "date") (format #f "~a" (etime->time_t expr))) + (#t + (mapconcat + (lambda (elm) (format #f "~a" (convert-xml elm))) expr "")))) + ((string? expr) (string->xml expr)) + ((symbol? expr) (format #f "~a" expr)) + ((number? expr) (number->string expr)) + (#t ".")))) + (msg->xml + (lambda () + (let ((expr (read))) + (if (not (eof-object? expr)) + (string-append (format #f "\n~a\n" (convert-xml expr)) (msg->xml)) + ""))))) + (format #f "\n\n~a" (msg->xml)))) + + +(define (sexp->json) + "Convert string INPUT to JSON, return the JSON (string)." + (letrec ((convert-json + (lambda* (expr #:optional parent) + (cond + ((property-list? expr) + (mapconcat + (lambda (pair) + (format #f "\n\t\"~a\": ~a" + (car pair) (convert-json (cdr pair) (car pair)))) + (plist->pairs expr) ", ")) + ((list? expr) + (cond + ((member parent '("from" "to" "cc" "bcc")) + (string-append "[" + (mapconcat (lambda (addr) + (format #f "{~a~a}" + (if (string? (car addr)) + (format #f "\"name\": \"~a\"," + (string->json (car addr))) "") + (if (string? (cdr addr)) + (format #f "\"email\": \"~a\"" + (string->json (cdr addr))) ""))) + expr ", ") + "]")) + ((string= parent "parts") "[]") ;; todo + ;; convert the crazy emacs time thingy to time_t... + ((string= parent "date") + (format #f "~a" (format #f "~a" (etime->time_t expr)))) + (#t + (string-append "[" + (mapconcat (lambda (elm) (format #f "~a" (convert-json elm))) expr ",") "]")))) + ((string? expr) + (format #f "\"~a\"" (string->json expr))) + ((symbol? expr) + (format #f "\"~a\"" expr)) + ((number? expr) (number->string expr)) + (#t ".")))) + (msg->json + (lambda (first) + (let ((expr (read))) + (if (not (eof-object? expr)) + (string-append (format #f "~a{~a\n}" + (if first "" ",\n") + (convert-json expr)) (msg->json #f)) + ""))))) + (format #f "[\n~a\n]" (msg->json #t)))) + +(define (main args) + (let* ((optionspec '((format (value #t)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu-sexp-convert " + "--format=\n" + "reads from standard-input and prints to standard output\n")) + (outformat (or (option-ref options 'format #f) + (begin (display msg) (exit 1))))) + (cond + ((string= outformat "xml") + (format #t "~a\n" (sexp->xml))) + ((string= outformat "json") + (format #t "~a\n" (sexp->json))) + (#t (begin + (display msg) + (exit 1)))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/contrib/mu.spec b/contrib/mu.spec new file mode 100644 index 0000000..1ed66d4 --- /dev/null +++ b/contrib/mu.spec @@ -0,0 +1,129 @@ + +# These refer to the release version +# When 0.9.9.6 gets out, remove the global pre line +%global pre pre2 +%global rel 1 + +Summary: A lightweight email search engine for Maildirs +Name: mu +Version: 0.9.9.6 +URL: https://github.com/djcb/mu +# From Packaging:NamingGuidelines for pre-relase versions: +# Release: 0.%{X}.%{alphatag} where %{X} is the release number +%if %{pre} +Release: 0.%{rel}.%{prerelease}%{?dist} +%else +Release: %{rel}%{?dist} +%endif + +License: GPLv3 +Group: Applications/Internet +BuildRoot: %{_tmppath}/%{name}-%{version}-build + +# Source is at ssaavedra repo because djcb has not yet this version tag created +Source0: http://github.com/ssaavedra/%{name}/archive/v%{version}%{?pre}.tar.gz +BuildRequires: emacs-el +BuildRequires: emacs +BuildRequires: gmime-devel +BuildRequires: guile-devel +BuildRequires: xapian-core-devel +BuildRequires: libuuid-devel +BuildRequires: texinfo +Requires: gmime +Requires: guile +Requires: xapian-core-libs +Requires: emacs-filesystem >= %{_emacs_version} + + +%description +E-mail is the 'flow' in the work flow of many people. Consequently, one spends a lot of time searching for old e-mails, to dig up some important piece of information. With people having tens of thousands of e-mails (or more), this is becoming harder and harder. How to find that one e-mail in an ever-growing haystack? +Enter mu. +'mu' is a set of command-line tools for Linux/Unix that enable you to quickly find the e-mails you are looking for, assuming that you store your e-mails in Maildirs (if you don't know what 'Maildirs' are, you are probably not using them). + +%package gtk +Group: Applications/Internet +Summary: GUI for using mu (called mug) +BuildRequires: gtk3-devel +BuildRequires: webkitgtk3-devel +Requires: gtk3 +Requires: gmime +Requires: webkitgtk3 +Requires: mu = %{version}-%{release} + +%description gtk +Mug is a simple GUI for mu from version 0.9. + +%package guile +Group: Applications/Internet +Summary: Guile scripting capabilities for mu +Requires: guile +Requires: mu = %{version}-%{release} +Requires(post): info +Requires(preun): info + +%description guile +Bindings for Guile to interact with mu. + + +%prep +%setup -n %{name}-%{version}%{?pre} -q + +%build +autoreconf -i +%configure +make %{?_smp_mflags} + +%install +rm -rf %{buildroot} +make install DESTDIR=%{buildroot} +install -p -c -m 755 %{_builddir}/%{buildsubdir}/toys/mug/mug %{buildroot}%{_bindir}/mug +cp -p %{_builddir}/%{buildsubdir}/mu4e/*.el %{buildroot}%{_emacs_sitelispdir}/mu4e/ +rm -f %{buildroot}%{_infodir}/dir + +%clean +rm -rf %{buildroot} + +%post +/sbin/install-info \ + --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || : +%preun +if [ $1 = 0 -a -f %{_infodir}/mu4e.info.gz ]; then + /sbin/install-info --delete \ + --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || : +fi + +%post guile +/sbin/install-info \ + --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || : + +%preun guile +if [ $1 = 0 -a -f %{_infodir}/mu-guile.info.gz ]; then + /sbin/install-info --delete \ + --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || : +fi + + +%files +%defattr(-,root,root) +%{_bindir}/mu +%{_mandir}/man1/* +%{_mandir}/man5/* +%{_datadir}/mu/* + +%{_emacs_sitelispdir}/mu4e +%{_emacs_sitelispdir}/mu4e/*.elc +%{_emacs_sitelispdir}/mu4e/*.el +%{_infodir}/mu4e.info.gz + +%files gtk +%{_bindir}/mug + +%files guile +%{_libdir}/libguile-mu.* +%{_datadir}/guile/site/2.0/mu/* +%{_datadir}/guile/site/2.0/mu.scm +%{_infodir}/mu-guile.info.gz + +%changelog +* Wed Feb 12 2014 Santiago Saavedra - 0.9.9.5-1 +- Create first SPEC. diff --git a/gtest.mk b/gtest.mk new file mode 100644 index 0000000..159576e --- /dev/null +++ b/gtest.mk @@ -0,0 +1,34 @@ +## Copyright (C) 2011 Dirk-Jan C. Binnema +## +## 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, 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +TEST_PROGS= + + +# NOTE: we set the locale/tz to some well-know values, so the tests +# (at least when running under 'make check') run in a predictable +# environment. There are specific tests different timezone, though. +# +test: all $(TEST_PROGS) + @export LC_ALL="en_US.utf8" + @export TZ="Europe/Helsinki" + @test -z "$(TEST_PROGS)" || gtester --verbose $(TEST_PROGS) || exit $$?; \ + test -z "$(SUBDIRS)" || \ + for subdir in $(SUBDIRS); do \ + test "$$subdir" = "." || \ + (cd ./$$subdir && $(MAKE) $(AM_MAKEFLAGS) $@ ) || exit $$? ; \ + done + +.PHONY: test gprof diff --git a/guile/Makefile.am b/guile/Makefile.am new file mode 100644 index 0000000..dd91108 --- /dev/null +++ b/guile/Makefile.am @@ -0,0 +1,97 @@ +## Copyright (C) 2011-2013 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +# note, we need top_builddir for snarfing with 'make distcheck' (ie., +# with separate builddir) +SUBDIRS= . mu scripts examples + +AM_CPPFLAGS= \ + -I. -I${top_builddir} -I${top_srcdir}/lib \ + ${GUILE_CFLAGS} \ + ${GLIB_CFLAGS} + +# don't use -Werror, as it might break on other compilers +# use -Wno-unused-parameters, because some callbacks may not +# really need all the params they get +AM_CFLAGS= \ + $(ASAN_CFLAGS) \ + ${WARN_CFLAGS} \ + -Wno-suggest-attribute=noreturn \ + -Wno-missing-prototypes \ + -Wno-missing-declarations + +AM_CXXFLAGS= \ + $(ASAN_CXXFLAGS) \ + ${WARN_CXXFLAGS} \ + -Wno-redundant-decls \ + -Wno-missing-declarations \ + -Wno-suggest-attribute=noreturn + +lib_LTLIBRARIES= \ + libguile-mu.la + +libguile_mu_la_SOURCES= \ + mu-guile.cc \ + mu-guile.hh \ + mu-guile-message.cc \ + mu-guile-message.hh + +libguile_mu_la_CFLAGS=$(AM_CFLAGS) +libguile_mu_la_CXXFLAGS=$(AM_CXXFLAGS) + +libguile_mu_la_LIBADD= \ + ${top_builddir}/lib/libmu.la \ + ${top_builddir}/lib/utils/libmu-utils.la \ + $(READLINE_LIBS) \ + ${GUILE_LIBS} + +libguile_mu_la_LDFLAGS= \ + $(ASAN_LDFLAGS) \ + -shared \ + -export-dynamic + +XFILES= \ + mu-guile.x \ + mu-guile-message.x + +info_TEXINFOS= \ + mu-guile.texi +mu_guile_TEXINFOS= \ + fdl.texi + +# we include pre-snarfed files now; see meson.build for explanation +# +# BUILT_SOURCES=$(XFILES) +# export CPP +# snarfcxxopts= $(DEFS) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) +# SUFFIXES = .x .doc +# .cc.x: +# $(AM_V_GEN) $(GUILE_SNARF) -o $@ $< $(snarfcxxopts) +#SNARF_DATA=$(XFILES) + +# FIXME: GUILE_SITEDIR would be better, but that +# breaks 'make distcheck' +scmdir=${prefix}/share/guile/site/${GUILE_EFFECTIVE_VERSION} +scm_DATA=mu.scm + +EXTRA_DIST=$(scm_DATA) $(XFILES) + +## Add -MG to make the .x magic work with auto-dep code. +MKDEP = $(CC) -M -MG $(snarfcppopts) + +#CLEANFILES=$(XFILES) diff --git a/guile/compile-scm.in b/guile/compile-scm.in new file mode 100644 index 0000000..04cc0f9 --- /dev/null +++ b/guile/compile-scm.in @@ -0,0 +1,22 @@ +#!/bin/sh +## Copyright (C) 2021 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +@abs_builddir@/build-env @guild@ compile "$@" + +# Local-Variables: +# mode: sh +# End: diff --git a/guile/examples/Makefile.am b/guile/examples/Makefile.am new file mode 100644 index 0000000..7e126c5 --- /dev/null +++ b/guile/examples/Makefile.am @@ -0,0 +1,23 @@ +## Copyright (C) 2012-2013 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +EXTRA_DIST= \ + msg-graphs \ + contacts-export \ + org2mu4e \ + mu-biff diff --git a/guile/examples/contacts-export b/guile/examples/contacts-export new file mode 100755 index 0000000..7e33c54 --- /dev/null +++ b/guile/examples/contacts-export @@ -0,0 +1,85 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; +;; Copyright (C) 2012 Dirk-Jan C. Binnema +;; +;; 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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +(use-modules (ice-9 getopt-long) (ice-9 format)) +(use-modules (srfi srfi-1)) +(use-modules (mu)) + +(define (sort-by-freq c1 c2) + (< (mu:frequency c1) (mu:frequency c2))) + +(define (sort-by-newness c1 c2) + (< (mu:last-seen c1) (mu:last-seen c2))) + +(define (main args) + (let* ((optionspec '( (muhome (value #t)) + (sort-by (value #t)) + (revert (value #f)) + (format (value #t)) + (limit (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: contacts-export [--help] [--muhome=] " + "--format= " + "--sort-by= [--revert] [--limit=]\n")) + (help (option-ref options 'help #f)) + (muhome (option-ref options 'muhome #f)) + (sort-by (or (option-ref options 'sort-by #f) "frequency")) + (revert (option-ref options 'revert #f)) + (form (or (option-ref options 'format #f) "plain")) + (limit (string->number (option-ref options 'limit "1000000")))) + (if help + (begin + (display msg) + (exit 0)) + (begin + (setlocale LC_ALL "") + (mu:initialize muhome) + (let* ((sort-func + (cond + ((string= sort-by "frequency") sort-by-freq) + ((string= sort-by "newness") sort-by-newness) + (else (begin (display msg) (exit 1))))) + (contacts '())) + ;; make a list of all contacts + (mu:for-each-contact + (lambda (c) (set! contacts (cons c contacts)))) + + ;; should we sort it? + (if sort-by + (set! contacts (sort! contacts + (if revert (negate sort-func) sort-func)))) + + ;; should we limit the number? + (if (and limit (< limit (length contacts))) + (set! contacts (take! contacts limit))) + ;; export! + (for-each + (lambda (c) + (format #t "~a\n" (mu:contact->string c form))) + contacts)))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/examples/msg-graphs b/guile/examples/msg-graphs new file mode 100755 index 0000000..654dd28 --- /dev/null +++ b/guile/examples/msg-graphs @@ -0,0 +1,133 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema +;; +;; 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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +(setlocale LC_ALL "") + +(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format)) +(use-modules (mu) (mu stats) (mu plot)) +;;(use-modules (mu) (mu message) (mu stats) (mu plot)) + +(define (per-hour expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (sort + (mu:tabulate + (lambda (msg) + (tm:hour (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per hour matching ~a" expr) "Hour" "Messages" output)) + +(define (per-day expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (mu:weekday-numbers->names + (sort (mu:tabulate + (lambda (msg) + (tm:wday (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per weekday matching ~a" expr) "Day" "Messages" output)) + +(define (per-month expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (mu:month-numbers->names + (sort + (mu:tabulate + (lambda (msg) + (tm:mon (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per month matching ~a" expr) "Month" "Messages" output)) + + +(define (per-year-month expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (sort (mu:tabulate + (lambda (msg) + (string->number + (format #f "~d~2'0d" + (+ 1900 (tm:year (localtime (mu:date msg)))) + (tm:mon (localtime (mu:date msg)))))) + expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year/month matching ~a" expr) + "Year/Month" "Messages" output)) + + + +(define (per-year expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (sort (mu:tabulate + (lambda (msg) + (+ 1900 (tm:year (localtime (mu:date msg))))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year matching ~a" expr) "Year" "Messages" output)) + + + +(define (main args) + (let* ((optionspec '( (muhome (value #t)) + (what (value #t)) + (text (value #f)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu-msg-stats [--help] [--text] " + "[--muhome=] " + "--what= [searchexpr]\n")) + (help (option-ref options 'help #f)) + (what (option-ref options 'what #f)) + (text (option-ref options 'text #f)) + ;; if `text' is `#f', use a graphical window by setting output to "wxt", + ;; else use text-mode plotting ("dumb") + (output (if text "dumb" "wxt")) + (muhome (option-ref options 'muhome #f)) + (restargs (option-ref options '() #f)) + (expr (if restargs (string-join restargs) ""))) + (if (or help (not what)) + (begin + (display msg) + (exit (if help 0 1)))) + (mu:initialize muhome) + (cond + ((string= what "per-hour") (per-hour expr output)) + ((string= what "per-day") (per-day expr output)) + ((string= what "per-month") (per-month expr output)) + ((string= what "per-year-month") (per-year-month expr output)) + ((string= what "per-year") (per-year expr output)) + (else (begin + (display msg) + (exit 1)))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/examples/mu-biff b/guile/examples/mu-biff new file mode 100755 index 0000000..bc6d507 --- /dev/null +++ b/guile/examples/mu-biff @@ -0,0 +1,59 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; +;; Copyright (C) 2012 Dirk-Jan C. Binnema +;; +;; 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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; script to list the message matching which are newer than +;; minutes + +;; use it, eg. like: +;; $ mu-biff --newer-than=`date +%s --date='5 minutes ago'` "maildir:/inbox" + + +(use-modules (ice-9 getopt-long) (ice-9 format)) +(use-modules (mu)) + +(define (main args) + (let* ((optionspec '((muhome (value #t)) + (newer-than (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu-biff [--help] [--muhome=]" + " [--newer-than=] ")) + (help (option-ref options 'help #f)) + (newer-than (string->number (option-ref options 'newer-than "0"))) + (muhome (option-ref options 'muhome #f)) + (query (string-concatenate (option-ref options '() '())))) + (if help + (begin (display msg) (newline) (exit 0)) + (begin + (mu:initialize muhome) + (mu:for-each-message + (lambda (msg) + (if (> (mu:timestamp msg) newer-than) + (format #t "~a ~a\n" + (mu:from msg) + (mu:subject msg)))) + query))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/examples/org2mu4e b/guile/examples/org2mu4e new file mode 100755 index 0000000..3556b9a --- /dev/null +++ b/guile/examples/org2mu4e @@ -0,0 +1,78 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; +;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema +;; +;; 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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(use-modules (ice-9 getopt-long) (ice-9 format)) +(use-modules (mu)) + +(define (display-org-header query) + "Print the header for the org-file for QUERY." + (format #t "* Messages matching '~a'\n\n" query)) + +(define (org-mu4e-link msg) + "Create a link for this message understandable by org-mu4e." + (let* ((subject ;; cleanup subject + (string-map + (lambda (kar) + (if (member kar '(#\] #\[)) #\space kar)) + (or (mu:subject msg) "No subject")))) + (format #f "[[mu4e:msgid:~a][~s]]" + (mu:message-id msg) subject))) + +(define (display-org-entry msg tag) + "Write an org entry for MSG." + (format #t "** ~a ~a\n\t~s\n\t~s\n" + (org-mu4e-link msg) + (if tag (string-concatenate `(":" ,tag "::")) "") + (or (mu:from msg) "?") + (let ((body (mu:body-txt msg))) + (if (not body) ;; get a 'summary' of the body text + "" + (string-map + (lambda (c) + (if (or (char=? c #\newline) (char=? c #\return)) + #\space + c)) + (substring body 0 (min (string-length body) 100))))))) + +(define (main args) + (let* ((optionspec '( (muhome (value #t)) + (tag (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu4e-org [--help] [--muhome=] [--tag=] ")) + (help (option-ref options 'help #f)) + (tag (option-ref options 'tag #f)) + (muhome (option-ref options 'muhome #f)) + (query (string-concatenate (option-ref options '() '())))) + (if help + (begin (display msg) (exit 0)) + (begin + (mu:initialize muhome) + (display-org-header query) + (mu:for-each-message + (lambda (msg) (display-org-entry msg tag)) + query))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/fdl.texi b/guile/fdl.texi new file mode 100644 index 0000000..96ce74e --- /dev/null +++ b/guile/fdl.texi @@ -0,0 +1,451 @@ +@c The GNU Free Documentation License. +@center Version 1.2, November 2002 + +@c This file is intended to be included within another document, +@c hence no sectioning command or @node. + +@display +Copyright @copyright{} 2000,2001,2002 Free Software Foundation, Inc. +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. +@end display + +@enumerate 0 +@item +PREAMBLE + +The purpose of this License is to make a manual, textbook, or other +functional and useful document @dfn{free} in the sense of freedom: to +assure everyone the effective freedom to copy and redistribute it, +with or without modifying it, either commercially or noncommercially. +Secondarily, this License preserves for the author and publisher a way +to get credit for their work, while not being considered responsible +for modifications made by others. + +This License is a kind of ``copyleft'', which means that derivative +works of the document must themselves be free in the same sense. It +complements the GNU General Public License, which is a copyleft +license designed for free software. + +We have designed this License in order to use it for manuals for free +software, because free software needs free documentation: a free +program should come with manuals providing the same freedoms that the +software does. But this License is not limited to software manuals; +it can be used for any textual work, regardless of subject matter or +whether it is published as a printed book. We recommend this License +principally for works whose purpose is instruction or reference. + +@item +APPLICABILITY AND DEFINITIONS + +This License applies to any manual or other work, in any medium, that +contains a notice placed by the copyright holder saying it can be +distributed under the terms of this License. Such a notice grants a +world-wide, royalty-free license, unlimited in duration, to use that +work under the conditions stated herein. The ``Document'', below, +refers to any such manual or work. Any member of the public is a +licensee, and is addressed as ``you''. You accept the license if you +copy, modify or distribute the work in a way requiring permission +under copyright law. + +A ``Modified Version'' of the Document means any work containing the +Document or a portion of it, either copied verbatim, or with +modifications and/or translated into another language. + +A ``Secondary Section'' is a named appendix or a front-matter section +of the Document that deals exclusively with the relationship of the +publishers or authors of the Document to the Document's overall +subject (or to related matters) and contains nothing that could fall +directly within that overall subject. (Thus, if the Document is in +part a textbook of mathematics, a Secondary Section may not explain +any mathematics.) The relationship could be a matter of historical +connection with the subject or with related matters, or of legal, +commercial, philosophical, ethical or political position regarding +them. + +The ``Invariant Sections'' are certain Secondary Sections whose titles +are designated, as being those of Invariant Sections, in the notice +that says that the Document is released under this License. If a +section does not fit the above definition of Secondary then it is not +allowed to be designated as Invariant. The Document may contain zero +Invariant Sections. If the Document does not identify any Invariant +Sections then there are none. + +The ``Cover Texts'' are certain short passages of text that are listed, +as Front-Cover Texts or Back-Cover Texts, in the notice that says that +the Document is released under this License. A Front-Cover Text may +be at most 5 words, and a Back-Cover Text may be at most 25 words. + +A ``Transparent'' copy of the Document means a machine-readable copy, +represented in a format whose specification is available to the +general public, that is suitable for revising the document +straightforwardly with generic text editors or (for images composed of +pixels) generic paint programs or (for drawings) some widely available +drawing editor, and that is suitable for input to text formatters or +for automatic translation to a variety of formats suitable for input +to text formatters. A copy made in an otherwise Transparent file +format whose markup, or absence of markup, has been arranged to thwart +or discourage subsequent modification by readers is not Transparent. +An image format is not Transparent if used for any substantial amount +of text. A copy that is not ``Transparent'' is called ``Opaque''. + +Examples of suitable formats for Transparent copies include plain +@sc{ascii} without markup, Texinfo input format, La@TeX{} input +format, @acronym{SGML} or @acronym{XML} using a publicly available +@acronym{DTD}, and standard-conforming simple @acronym{HTML}, +PostScript or @acronym{PDF} designed for human modification. Examples +of transparent image formats include @acronym{PNG}, @acronym{XCF} and +@acronym{JPG}. Opaque formats include proprietary formats that can be +read and edited only by proprietary word processors, @acronym{SGML} or +@acronym{XML} for which the @acronym{DTD} and/or processing tools are +not generally available, and the machine-generated @acronym{HTML}, +PostScript or @acronym{PDF} produced by some word processors for +output purposes only. + +The ``Title Page'' means, for a printed book, the title page itself, +plus such following pages as are needed to hold, legibly, the material +this License requires to appear in the title page. For works in +formats which do not have any title page as such, ``Title Page'' means +the text near the most prominent appearance of the work's title, +preceding the beginning of the body of the text. + +A section ``Entitled XYZ'' means a named subunit of the Document whose +title either is precisely XYZ or contains XYZ in parentheses following +text that translates XYZ in another language. (Here XYZ stands for a +specific section name mentioned below, such as ``Acknowledgements'', +``Dedications'', ``Endorsements'', or ``History''.) To ``Preserve the Title'' +of such a section when you modify the Document means that it remains a +section ``Entitled XYZ'' according to this definition. + +The Document may include Warranty Disclaimers next to the notice which +states that this License applies to the Document. These Warranty +Disclaimers are considered to be included by reference in this +License, but only as regards disclaiming warranties: any other +implication that these Warranty Disclaimers may have is void and has +no effect on the meaning of this License. + +@item +VERBATIM COPYING + +You may copy and distribute the Document in any medium, either +commercially or noncommercially, provided that this License, the +copyright notices, and the license notice saying this License applies +to the Document are reproduced in all copies, and that you add no other +conditions whatsoever to those of this License. You may not use +technical measures to obstruct or control the reading or further +copying of the copies you make or distribute. However, you may accept +compensation in exchange for copies. If you distribute a large enough +number of copies you must also follow the conditions in section 3. + +You may also lend copies, under the same conditions stated above, and +you may publicly display copies. + +@item +COPYING IN QUANTITY + +If you publish printed copies (or copies in media that commonly have +printed covers) of the Document, numbering more than 100, and the +Document's license notice requires Cover Texts, you must enclose the +copies in covers that carry, clearly and legibly, all these Cover +Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on +the back cover. Both covers must also clearly and legibly identify +you as the publisher of these copies. The front cover must present +the full title with all words of the title equally prominent and +visible. You may add other material on the covers in addition. +Copying with changes limited to the covers, as long as they preserve +the title of the Document and satisfy these conditions, can be treated +as verbatim copying in other respects. + +If the required texts for either cover are too voluminous to fit +legibly, you should put the first ones listed (as many as fit +reasonably) on the actual cover, and continue the rest onto adjacent +pages. + +If you publish or distribute Opaque copies of the Document numbering +more than 100, you must either include a machine-readable Transparent +copy along with each Opaque copy, or state in or with each Opaque copy +a computer-network location from which the general network-using +public has access to download using public-standard network protocols +a complete Transparent copy of the Document, free of added material. +If you use the latter option, you must take reasonably prudent steps, +when you begin distribution of Opaque copies in quantity, to ensure +that this Transparent copy will remain thus accessible at the stated +location until at least one year after the last time you distribute an +Opaque copy (directly or through your agents or retailers) of that +edition to the public. + +It is requested, but not required, that you contact the authors of the +Document well before redistributing any large number of copies, to give +them a chance to provide you with an updated version of the Document. + +@item +MODIFICATIONS + +You may copy and distribute a Modified Version of the Document under +the conditions of sections 2 and 3 above, provided that you release +the Modified Version under precisely this License, with the Modified +Version filling the role of the Document, thus licensing distribution +and modification of the Modified Version to whoever possesses a copy +of it. In addition, you must do these things in the Modified Version: + +@enumerate A +@item +Use in the Title Page (and on the covers, if any) a title distinct +from that of the Document, and from those of previous versions +(which should, if there were any, be listed in the History section +of the Document). You may use the same title as a previous version +if the original publisher of that version gives permission. + +@item +List on the Title Page, as authors, one or more persons or entities +responsible for authorship of the modifications in the Modified +Version, together with at least five of the principal authors of the +Document (all of its principal authors, if it has fewer than five), +unless they release you from this requirement. + +@item +State on the Title page the name of the publisher of the +Modified Version, as the publisher. + +@item +Preserve all the copyright notices of the Document. + +@item +Add an appropriate copyright notice for your modifications +adjacent to the other copyright notices. + +@item +Include, immediately after the copyright notices, a license notice +giving the public permission to use the Modified Version under the +terms of this License, in the form shown in the Addendum below. + +@item +Preserve in that license notice the full lists of Invariant Sections +and required Cover Texts given in the Document's license notice. + +@item +Include an unaltered copy of this License. + +@item +Preserve the section Entitled ``History'', Preserve its Title, and add +to it an item stating at least the title, year, new authors, and +publisher of the Modified Version as given on the Title Page. If +there is no section Entitled ``History'' in the Document, create one +stating the title, year, authors, and publisher of the Document as +given on its Title Page, then add an item describing the Modified +Version as stated in the previous sentence. + +@item +Preserve the network location, if any, given in the Document for +public access to a Transparent copy of the Document, and likewise +the network locations given in the Document for previous versions +it was based on. These may be placed in the ``History'' section. +You may omit a network location for a work that was published at +least four years before the Document itself, or if the original +publisher of the version it refers to gives permission. + +@item +For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve +the Title of the section, and preserve in the section all the +substance and tone of each of the contributor acknowledgements and/or +dedications given therein. + +@item +Preserve all the Invariant Sections of the Document, +unaltered in their text and in their titles. Section numbers +or the equivalent are not considered part of the section titles. + +@item +Delete any section Entitled ``Endorsements''. Such a section +may not be included in the Modified Version. + +@item +Do not retitle any existing section to be Entitled ``Endorsements'' or +to conflict in title with any Invariant Section. + +@item +Preserve any Warranty Disclaimers. +@end enumerate + +If the Modified Version includes new front-matter sections or +appendices that qualify as Secondary Sections and contain no material +copied from the Document, you may at your option designate some or all +of these sections as invariant. To do this, add their titles to the +list of Invariant Sections in the Modified Version's license notice. +These titles must be distinct from any other section titles. + +You may add a section Entitled ``Endorsements'', provided it contains +nothing but endorsements of your Modified Version by various +parties---for example, statements of peer review or that the text has +been approved by an organization as the authoritative definition of a +standard. + +You may add a passage of up to five words as a Front-Cover Text, and a +passage of up to 25 words as a Back-Cover Text, to the end of the list +of Cover Texts in the Modified Version. Only one passage of +Front-Cover Text and one of Back-Cover Text may be added by (or +through arrangements made by) any one entity. If the Document already +includes a cover text for the same cover, previously added by you or +by arrangement made by the same entity you are acting on behalf of, +you may not add another; but you may replace the old one, on explicit +permission from the previous publisher that added the old one. + +The author(s) and publisher(s) of the Document do not by this License +give permission to use their names for publicity for or to assert or +imply endorsement of any Modified Version. + +@item +COMBINING DOCUMENTS + +You may combine the Document with other documents released under this +License, under the terms defined in section 4 above for modified +versions, provided that you include in the combination all of the +Invariant Sections of all of the original documents, unmodified, and +list them all as Invariant Sections of your combined work in its +license notice, and that you preserve all their Warranty Disclaimers. + +The combined work need only contain one copy of this License, and +multiple identical Invariant Sections may be replaced with a single +copy. If there are multiple Invariant Sections with the same name but +different contents, make the title of each such section unique by +adding at the end of it, in parentheses, the name of the original +author or publisher of that section if known, or else a unique number. +Make the same adjustment to the section titles in the list of +Invariant Sections in the license notice of the combined work. + +In the combination, you must combine any sections Entitled ``History'' +in the various original documents, forming one section Entitled +``History''; likewise combine any sections Entitled ``Acknowledgements'', +and any sections Entitled ``Dedications''. You must delete all +sections Entitled ``Endorsements.'' + +@item +COLLECTIONS OF DOCUMENTS + +You may make a collection consisting of the Document and other documents +released under this License, and replace the individual copies of this +License in the various documents with a single copy that is included in +the collection, provided that you follow the rules of this License for +verbatim copying of each of the documents in all other respects. + +You may extract a single document from such a collection, and distribute +it individually under this License, provided you insert a copy of this +License into the extracted document, and follow this License in all +other respects regarding verbatim copying of that document. + +@item +AGGREGATION WITH INDEPENDENT WORKS + +A compilation of the Document or its derivatives with other separate +and independent documents or works, in or on a volume of a storage or +distribution medium, is called an ``aggregate'' if the copyright +resulting from the compilation is not used to limit the legal rights +of the compilation's users beyond what the individual works permit. +When the Document is included in an aggregate, this License does not +apply to the other works in the aggregate which are not themselves +derivative works of the Document. + +If the Cover Text requirement of section 3 is applicable to these +copies of the Document, then if the Document is less than one half of +the entire aggregate, the Document's Cover Texts may be placed on +covers that bracket the Document within the aggregate, or the +electronic equivalent of covers if the Document is in electronic form. +Otherwise they must appear on printed covers that bracket the whole +aggregate. + +@item +TRANSLATION + +Translation is considered a kind of modification, so you may +distribute translations of the Document under the terms of section 4. +Replacing Invariant Sections with translations requires special +permission from their copyright holders, but you may include +translations of some or all Invariant Sections in addition to the +original versions of these Invariant Sections. You may include a +translation of this License, and all the license notices in the +Document, and any Warranty Disclaimers, provided that you also include +the original English version of this License and the original versions +of those notices and disclaimers. In case of a disagreement between +the translation and the original version of this License or a notice +or disclaimer, the original version will prevail. + +If a section in the Document is Entitled ``Acknowledgements'', +``Dedications'', or ``History'', the requirement (section 4) to Preserve +its Title (section 1) will typically require changing the actual +title. + +@item +TERMINATION + +You may not copy, modify, sublicense, or distribute the Document except +as expressly provided for under this License. Any other attempt to +copy, modify, sublicense or distribute the Document is void, and will +automatically terminate your rights under this License. However, +parties who have received copies, or rights, from you under this +License will not have their licenses terminated so long as such +parties remain in full compliance. + +@item +FUTURE REVISIONS OF THIS LICENSE + +The Free Software Foundation may publish new, revised versions +of the GNU Free Documentation License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. See +@uref{http://www.gnu.org/copyleft/}. + +Each version of the License is given a distinguishing version number. +If the Document specifies that a particular numbered version of this +License ``or any later version'' applies to it, you have the option of +following the terms and conditions either of that specified version or +of any later version that has been published (not as a draft) by the +Free Software Foundation. If the Document does not specify a version +number of this License, you may choose any version ever published (not +as a draft) by the Free Software Foundation. +@end enumerate + +@page +@heading ADDENDUM: How to use this License for your documents + +To use this License in a document you have written, include a copy of +the License in the document and put the following copyright and +license notices just after the title page: + +@smallexample +@group + Copyright (C) @var{year} @var{your name}. + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU Free Documentation License, Version 1.2 + or any later version published by the Free Software Foundation; + with no Invariant Sections, no Front-Cover Texts, and no Back-Cover + Texts. A copy of the license is included in the section entitled ``GNU + Free Documentation License''. +@end group +@end smallexample + +If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, +replace the ``with@dots{}Texts.'' line with this: + +@smallexample +@group + with the Invariant Sections being @var{list their titles}, with + the Front-Cover Texts being @var{list}, and with the Back-Cover Texts + being @var{list}. +@end group +@end smallexample + +If you have Invariant Sections without Cover Texts, or some other +combination of the three, merge those two alternatives to suit the +situation. + +If your document contains nontrivial examples of program code, we +recommend releasing these examples in parallel under your choice of +free software license, such as the GNU General Public License, +to permit their use in free software. + +@c Local Variables: +@c ispell-local-pdict: "ispell-dict" +@c End: + diff --git a/guile/meson.build b/guile/meson.build new file mode 100644 index 0000000..a427644 --- /dev/null +++ b/guile/meson.build @@ -0,0 +1,113 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# +# create a shell script for compiling from the source dirs +compile_scm_conf = configuration_data() +compile_scm_conf.set('abs_builddir', meson.current_build_dir()) +compile_scm_conf.set('guild', 'guild') +compile_scm=configure_file( + input: 'compile-scm.in', + output: 'compile-scm', + configuration: compile_scm_conf, + install: false +) +run_command('chmod', '+x', compile_scm, check: true) +scm_compiler=join_paths(meson.current_build_dir(), 'compile-scm') + +# +# NOTE: snarfing works but you get: +# ,---- +# | cc1plus: warning: command-line option ‘-std=gnu11’ is valid for C/ObjC +# | but not for C++ +# `---- +# this is because the snarf-script hardcodes the '-std=gnu11' but we're +# building for c++; even worse, e.g. on some MacOS, the warning is a +# hard error. +# +# We can override flag through a env variable CPP; but then we _also_ need to +# override the compiler, so e.g. CPP="g++ -std=c++17'; but it's a bit +# hairy/ugly/fragile to derive the raw compiler name in meson; also the +# generator expression doesn't take an 'env:' parameter, so we'd need +# to rewrite using custom_target... +# +# for now, we avoid all that by simply including the generated files. +do_snarf=false + +if do_snarf + snarf = find_program('guile-snarf3.0','guile-snarf') + # there must be a better way of feeding the include paths to snarf... + snarf_args=['-o', '@OUTPUT@', '@INPUT@', '-I' + meson.current_source_dir() + '/..', + '-I' + meson.current_source_dir() + '/../lib', + '-I' + meson.current_build_dir() + '/..'] + snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('includedir'), + 'glib-2.0') + snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('libdir'), + 'glib-2.0', 'include') + snarf_args += '-I' + join_paths(guile_dep.get_pkgconfig_variable('includedir'), + 'guile', '3.0') + snarf_gen=generator(snarf, + output: '@BASENAME@.x', + arguments: snarf_args) + snarf_srcs=['mu-guile.cc', 'mu-guile-message.cc'] + snarf_x=snarf_gen.process(snarf_srcs) +else + snarf_x = [ 'mu-guile-message.x', 'mu-guile.x' ] +endif + +lib_guile_mu = shared_module( + 'guile-mu', + [ 'mu-guile.cc', + 'mu-guile-message.cc' ], + dependencies: [guile_dep, glib_dep, lib_mu_dep, config_h_dep, thread_dep ], + install: true) + +if makeinfo.found() + custom_target('mu_guile_info', + input: 'mu-guile.texi', + output: 'mu-guile.info', + install: true, + install_dir: infodir, + command: [makeinfo, + '-o', join_paths(meson.current_build_dir(), 'mu-guile.info'), + join_paths(meson.current_source_dir(), 'mu-guile.texi'), + '-I', join_paths(meson.current_build_dir(), '..')]) + + if install_info.found() + meson.add_install_script(install_info_script, 'share/info', 'mu-guile.info') + endif +endif + +guile_scm_dir=join_paths(datadir, 'guile', 'site', '3.0', 'mu') +install_data(['mu.scm','mu/script.scm', 'mu/message.scm', 'mu/stats.scm', 'mu/plot.scm'], + install_dir: guile_scm_dir) + + +mu_guile_scripts=[ + join_paths('scripts', 'find-dups.scm'), + join_paths('scripts', 'msgs-count.scm'), + join_paths('scripts', 'msgs-per-day.scm'), + join_paths('scripts', 'msgs-per-hour.scm'), + join_paths('scripts', 'msgs-per-month.scm'), + join_paths('scripts', 'msgs-per-year-month.scm'), + join_paths('scripts', 'msgs-per-year.scm') +] +mu_guile_script_dir=join_paths(datadir, 'mu', 'scripts') +install_data(mu_guile_scripts, install_dir: mu_guile_script_dir) + +guile_builddir=meson.current_build_dir() + +subdir('tests') diff --git a/guile/mu-guile-message.cc b/guile/mu-guile-message.cc new file mode 100644 index 0000000..184bd91 --- /dev/null +++ b/guile/mu-guile-message.cc @@ -0,0 +1,484 @@ +/* +** Copyright (C) 2011-2021 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include "mu-guile-message.hh" + +#include +#include "message/mu-message.hh" +#include "utils/mu-utils.hh" +#include + +#include +#include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wredundant-decls" +#include +#pragma GCC diagnostic pop + +#include "mu-guile.hh" + +#include +#include +#include + +using namespace Mu; + +/* pseudo field, not in Xapian */ +constexpr auto MU_GUILE_MSG_FIELD_ID_TIMESTAMP = Field::id_size() + 1; + +/* some symbols */ +static SCM SYMB_PRIO_LOW, SYMB_PRIO_NORMAL, SYMB_PRIO_HIGH; +static std::array SYMB_FLAGS; +static SCM SYMB_CONTACT_TO, SYMB_CONTACT_CC, SYMB_CONTACT_BCC, SYMB_CONTACT_FROM; +static long MSG_TAG; + + +using MessageSPtr = std::unique_ptr; + +static gboolean +mu_guile_scm_is_msg(SCM scm) +{ + return SCM_NIMP(scm) && (long)SCM_CAR(scm) == MSG_TAG; +} + +static SCM +message_scm_create(Xapian::Document&& doc) +{ + /* placement-new */ + + void *scm_mem{scm_gc_malloc(sizeof(Message), "msg")}; + Message* msgp = new(scm_mem)Message(std::move(doc)); + + SCM_RETURN_NEWSMOB(MSG_TAG, msgp); +} + +static const Message* +message_from_scm(SCM msg_smob) +{ + return reinterpret_cast(SCM_CDR(msg_smob)); +} + +static size_t +message_scm_free(SCM msg_smob) +{ + if (auto msg = message_from_scm(msg_smob); msg) + msg->~Message(); + + return sizeof(Message); +} + +static int +message_scm_print(SCM msg_smob, SCM port, scm_print_state* pstate) +{ + scm_puts("#path().c_str(), port); + + scm_puts(">", port); + return 1; +} + +struct FlagData { + Flags flags; + SCM lst; +}; + +#define MU_GUILE_INITIALIZED_OR_ERROR \ + do { \ + if (!(mu_guile_initialized())) { \ + mu_guile_error(FUNC_NAME, \ + 0, \ + "mu not initialized; call mu:initialize", \ + SCM_UNDEFINED); \ + return SCM_UNSPECIFIED; \ + } \ + } while (0) + + +static SCM +get_flags_scm(const Message& msg) +{ + SCM lst{SCM_EOL}; + const auto flags{msg.flags()}; + + for (auto i = 0; i != AllMessageFlagInfos.size(); ++i) { + const auto& info{AllMessageFlagInfos.at(i)}; + if (any_of(info.flag & flags)) + scm_append_x(scm_list_2(lst, scm_list_1(SYMB_FLAGS.at(i)))); + } + + return lst; +} + +static SCM +get_prio_scm(const Message& msg) +{ + switch (msg.priority()) { + case Priority::Low: return SYMB_PRIO_LOW; + case Priority::Normal: return SYMB_PRIO_NORMAL; + case Priority::High: return SYMB_PRIO_HIGH; + + default: g_return_val_if_reached(SCM_UNDEFINED); + } +} + +static SCM +msg_string_list_field(const Message& msg, Field::Id field_id) +{ + SCM scmlst{SCM_EOL}; + for (auto&& val: msg.document().string_vec_value(field_id)) { + SCM item; + item = scm_list_1(mu_guile_scm_from_string(val)); + scmlst = scm_append_x(scm_list_2(scmlst, item)); + } + + return scmlst; +} + +static SCM +msg_contact_list_field(const Message& msg, Field::Id field_id) +{ + return scm_from_utf8_string( + to_string(msg.document().contacts_value(field_id)).c_str()); +} + +static SCM +get_body(const Message& msg, bool html) +{ + if (const auto body = html ? msg.body_html() : msg.body_text(); body) + return mu_guile_scm_from_string(*body); + else + return SCM_BOOL_F; +} + +SCM_DEFINE(get_field, + "mu:c:get-field", + 2, + 0, + 0, + (SCM MSG, SCM FIELD), + "Get the field FIELD from message MSG.\n") +#define FUNC_NAME s_get_field +{ + SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + auto msg{message_from_scm(MSG)}; + SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME); + + SCM_ASSERT(scm_integer_p(FIELD), FIELD, SCM_ARG2, FUNC_NAME); + const auto field_opt{field_from_number(static_cast(scm_to_int(FIELD)))}; + SCM_ASSERT(!!field_opt, FIELD, SCM_ARG2, FUNC_NAME); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (field_opt->id) { + case Field::Id::Priority: + return get_prio_scm(*msg); + case Field::Id::Flags: + return get_flags_scm(*msg); + case Field::Id::BodyText: + return get_body(*msg, false); + default: break; + } +#pragma GCC diagnostic pop + + switch (field_opt->type) { + case Field::Type::String: + return mu_guile_scm_from_string(msg->document().string_value(field_opt->id)); + case Field::Type::ByteSize: + case Field::Type::TimeT: + case Field::Type::Integer: + return scm_from_uint(msg->document().integer_value(field_opt->id)); + case Field::Type::StringList: + return msg_string_list_field(*msg, field_opt->id); + case Field::Type::ContactList: + return msg_contact_list_field(*msg, field_opt->id); + default: + SCM_ASSERT(0, FIELD, SCM_ARG2, FUNC_NAME); + } +} +#undef FUNC_NAME + +static SCM +contacts_to_list(const Message& msg, Option field_id) +{ + SCM list{SCM_EOL}; + + const auto contacts{field_id ? + msg.document().contacts_value(*field_id) : + msg.all_contacts()}; + + for (auto&& contact: contacts) { + SCM item{scm_list_1( + scm_cons(mu_guile_scm_from_string(contact.name), + mu_guile_scm_from_string(contact.email)))}; + list = scm_append_x(scm_list_2(list, item)); + } + + return list; +} + +SCM_DEFINE(get_contacts, + "mu:c:get-contacts", + 2, + 0, + 0, + (SCM MSG, SCM CONTACT_TYPE), + "Get a list of contact information pairs.\n") +#define FUNC_NAME s_get_contacts +{ + SCM list; + + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + auto msg{message_from_scm(MSG)}; + SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME); + + SCM_ASSERT(scm_symbol_p(CONTACT_TYPE) || scm_is_bool(CONTACT_TYPE), + CONTACT_TYPE, + SCM_ARG2, + FUNC_NAME); + + if (CONTACT_TYPE == SCM_BOOL_F) + return SCM_UNSPECIFIED; /* nothing to do */ + + Option field_id; + if (CONTACT_TYPE == SCM_BOOL_T) + field_id = {}; /* get all */ + else { + if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_TO)) + field_id = Field::Id::To; + else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_CC)) + field_id = Field::Id::Cc; + else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_BCC)) + field_id = Field::Id::Bcc; + else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_FROM)) + field_id = Field::Id::From; + else { + mu_guile_error(FUNC_NAME, 0, "invalid contact type", SCM_UNDEFINED); + return SCM_UNSPECIFIED; + } + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" + list = contacts_to_list(*msg, field_id); +#pragma GCC diagnostic pop + + /* explicitly close the file backend, so we won't run out of fds */ + + + return list; +} +#undef FUNC_NAME + +SCM_DEFINE(get_parts, + "mu:c:get-parts", + 1, + 1, + 0, + (SCM MSG, SCM ATTS_ONLY), + "Get the list of mime-parts for MSG. If ATTS_ONLY is #t, only" + "get parts that are (look like) attachments. The resulting list has " + "elements which are list of the form (index name mime-type size).\n") +#define FUNC_NAME s_get_parts +{ + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + auto msg{message_from_scm(MSG)}; + SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME); + SCM_ASSERT(scm_is_bool(ATTS_ONLY), ATTS_ONLY, SCM_ARG2, FUNC_NAME); + + SCM attlist = SCM_EOL; /* empty list */ + bool attachments_only = ATTS_ONLY == SCM_BOOL_T ? TRUE : FALSE; + + size_t n{}; + for (auto&& part: msg->parts()) { + + if (attachments_only && !part.is_attachment()) + continue; + + const auto mime_type{part.mime_type()}; + const auto filename{part.cooked_filename()}; + + SCM elm = scm_list_5( + /* msg */ + mu_guile_scm_from_string(msg->path().c_str()), + /* index */ + scm_from_uint(n++), + /* filename or #f */ + filename ? mu_guile_scm_from_string(*filename) : SCM_BOOL_F, + /* mime-type */ + mime_type ? mu_guile_scm_from_string(*mime_type) : SCM_BOOL_F, + /* size */ + part.size() > 0 ? scm_from_uint(part.size()) : SCM_BOOL_F); + + attlist = scm_cons(elm, attlist); + } + + /* explicitly close the file backend, so we won't run of fds */ + msg->unload_mime_message(); + + return attlist; +} +#undef FUNC_NAME + +SCM_DEFINE(get_header, + "mu:c:get-header", + 2, + 0, + 0, + (SCM MSG, SCM HEADER), + "Get an arbitrary HEADER from MSG.\n") +#define FUNC_NAME s_get_header +{ + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + auto msg{message_from_scm(MSG)}; + SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME); + + SCM_ASSERT(scm_is_string(HEADER) || HEADER == SCM_UNDEFINED, HEADER, SCM_ARG2, FUNC_NAME); + + char *header = scm_to_utf8_string(HEADER); + SCM val = mu_guile_scm_from_string(msg->header(header).value_or("")); + free(header); + + /* explicitly close the file backend, so we won't run of fds */ + msg->unload_mime_message(); + + return val; +} +#undef FUNC_NAME +SCM_DEFINE(for_each_message, + "mu:c:for-each-message", + 3, + 0, + 0, + (SCM FUNC, SCM EXPR, SCM MAXNUM), + "Call FUNC for each msg in the message store matching EXPR. EXPR is" + "either a string containing a mu search expression or a boolean; in the former " + "case, limit the messages to only those matching the expression, in the " + "latter case, match /all/ messages if the EXPR equals #t, and match " + "none if EXPR equals #f.") +#define FUNC_NAME s_for_each_message +{ + char* expr{}; + + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT(scm_procedure_p(FUNC), FUNC, SCM_ARG1, FUNC_NAME); + SCM_ASSERT(scm_is_bool(EXPR) || scm_is_string(EXPR), EXPR, SCM_ARG2, FUNC_NAME); + SCM_ASSERT(scm_is_integer(MAXNUM), MAXNUM, SCM_ARG3, FUNC_NAME); + + if (EXPR == SCM_BOOL_F) + return SCM_UNSPECIFIED; /* nothing to do */ + + if (EXPR == SCM_BOOL_T) + expr = strdup("\"\""); /* note, "" matches *all* messages */ + else + expr = scm_to_utf8_string(EXPR); + + const auto res = mu_guile_store().run_query(expr,{}, {}, scm_to_int(MAXNUM)); + free(expr); + if (!res) + return SCM_UNSPECIFIED; + + for (auto&& mi : *res) { + if (auto xdoc{mi.document()}; xdoc) { + scm_call_1(FUNC, message_scm_create(std::move(xdoc.value()))); + } + } + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +static SCM +register_symbol(const char* name) +{ + SCM scm; + + scm = scm_from_utf8_symbol(name); + scm_c_define(name, scm); + scm_c_export(name, NULL); + + return scm; +} + +static void +define_symbols(void) +{ + SYMB_CONTACT_TO = register_symbol("mu:contact:to"); + SYMB_CONTACT_CC = register_symbol("mu:contact:cc"); + SYMB_CONTACT_FROM = register_symbol("mu:contact:from"); + SYMB_CONTACT_BCC = register_symbol("mu:contact:bcc"); + + SYMB_PRIO_LOW = register_symbol("mu:prio:low"); + SYMB_PRIO_NORMAL = register_symbol("mu:prio:normal"); + SYMB_PRIO_HIGH = register_symbol("mu:prio:high"); + + for (auto i = 0U; i != AllMessageFlagInfos.size(); ++i) { + const auto& info{AllMessageFlagInfos.at(i)}; + const auto name = "mu:flag:" + std::string{info.name}; + SYMB_FLAGS[i] = register_symbol(name.c_str()); + } +} +static void +define_vars(void) +{ + field_for_each([](auto&& field){ + + auto defvar = [&](auto&& fname, auto&& ffield) { + const auto name{"mu:field:" + std::string{fname}}; + scm_c_define(name.c_str(), scm_from_uint(field.value_no())); + scm_c_export(name.c_str(), NULL); + }; + + // define for both name and (if exists) alias. + if (!field.name.empty()) + defvar(field.name, field); + if (!field.alias.empty()) + defvar(field.alias, field); + }); + + /* non-Xapian field: timestamp */ + scm_c_define("mu:field:timestamp", + scm_from_uint(MU_GUILE_MSG_FIELD_ID_TIMESTAMP)); + scm_c_export("mu:field:timestamp", NULL); + +} + +void* +mu_guile_message_init(void* data) +{ + MSG_TAG = scm_make_smob_type("message", sizeof(Message)); + + scm_set_smob_free(MSG_TAG, message_scm_free); + scm_set_smob_print(MSG_TAG, message_scm_print); + + define_vars(); + define_symbols(); + +#ifndef SCM_MAGIC_SNARFER +#include "mu-guile-message.x" +#endif /*SCM_MAGIC_SNARFER*/ + + return NULL; +} diff --git a/guile/mu-guile-message.hh b/guile/mu-guile-message.hh new file mode 100644 index 0000000..0e7201d --- /dev/null +++ b/guile/mu-guile-message.hh @@ -0,0 +1,34 @@ +/* +** Copyright (C) 2011-2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_GUILE_MESSAGE_H__ +#define MU_GUILE_MESSAGE_H__ + +/** + * Initialize this mu guile module. + * + * @param data +q * + * @return + */ +extern "C" { +void* mu_guile_message_init(void* data); +} + +#endif /*MU_GUILE_MESSAGE_HH__*/ diff --git a/guile/mu-guile-message.x b/guile/mu-guile-message.x new file mode 100644 index 0000000..6127b39 --- /dev/null +++ b/guile/mu-guile-message.x @@ -0,0 +1,6 @@ +/* cpp arguments: mu-guile-message.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */ +scm_c_define_gsubr (s_get_field, 2, 0, 0, (scm_t_subr) get_field);; +scm_c_define_gsubr (s_get_contacts, 2, 0, 0, (scm_t_subr) get_contacts);; +scm_c_define_gsubr (s_get_parts, 1, 1, 0, (scm_t_subr) get_parts);; +scm_c_define_gsubr (s_get_header, 2, 0, 0, (scm_t_subr) get_header);; +scm_c_define_gsubr (s_for_each_message, 3, 0, 0, (scm_t_subr) for_each_message);; diff --git a/guile/mu-guile.cc b/guile/mu-guile.cc new file mode 100644 index 0000000..ff62273 --- /dev/null +++ b/guile/mu-guile.cc @@ -0,0 +1,258 @@ +/* +** Copyright (C) 2011-2021 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-guile.hh" + +#include +#include +#include + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wredundant-decls" +#include +#pragma GCC diagnostic pop + +#include +#include +#include + +using namespace Mu; + +SCM +mu_guile_scm_from_string(const std::string& str) +{ + if (str.empty()) + return SCM_BOOL_F; + else + return scm_from_stringn(str.c_str(), str.size(), + "UTF-8", + SCM_FAILED_CONVERSION_QUESTION_MARK); +} + +SCM +mu_guile_error(const char* func_name, int status, const char* fmt, SCM args) +{ + scm_error_scm(scm_from_locale_symbol("MuError"), + scm_from_utf8_string(func_name ? func_name : ""), + scm_from_utf8_string(fmt), + args, + scm_list_1(scm_from_int(status))); + + return SCM_UNSPECIFIED; +} + +SCM +mu_guile_g_error(const char* func_name, GError* err) +{ + scm_error_scm(scm_from_locale_symbol("MuError"), + scm_from_utf8_string(func_name), + scm_from_utf8_string(err ? err->message : "error"), + SCM_UNDEFINED, + SCM_UNDEFINED); + + return SCM_UNSPECIFIED; +} + +/* there can be only one */ + +static Option StoreSingleton = Nothing; + +static bool +mu_guile_init_instance(const char* muhome) +try { + setlocale(LC_ALL, ""); + if (!mu_runtime_init(muhome, "guile", true) || StoreSingleton) + return FALSE; + + const auto path{mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB)}; + auto store = Store::make(path); + if (!store) { + g_critical("error creating store @ %s: %s", path, store.error().what()); + throw store.error(); + } else + StoreSingleton.emplace(std::move(store.value())); + + g_debug("mu-guile: opened store @ %s (n=%zu); maildir: %s", + StoreSingleton->properties().database_path.c_str(), + StoreSingleton->size(), + StoreSingleton->properties().root_maildir.c_str()); + + return true; + +} catch (const Xapian::Error& xerr) { + g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str()); + return false; +} catch (const std::runtime_error& re) { + g_critical("%s: error: %s", __func__, re.what()); + return false; +} catch (const std::exception& e) { + g_critical("%s: caught exception: %s", __func__, e.what()); + return false; +} catch (...) { + g_critical("%s: caught exception", __func__); + return false; +} + +static void +mu_guile_uninit_instance() +{ + StoreSingleton.reset(); + + mu_runtime_uninit(); +} + +Mu::Store& +mu_guile_store() +{ + if (!StoreSingleton) + g_error("mu guile not initialized"); + + return StoreSingleton.value(); +} + +gboolean +mu_guile_initialized() +{ + g_debug("initialized ? %u", !!StoreSingleton); + + return !!StoreSingleton; +} + +SCM_DEFINE_PUBLIC(mu_initialize, + "mu:initialize", + 0, + 1, + 0, + (SCM MUHOME), + "Initialize mu - needed before you call any of the other " + "functions. Optionally, you can provide MUHOME which should be an " + "absolute path to your mu home directory " + "-- typically, the default, ~/.cache/mu, should be just fine.") +#define FUNC_NAME s_mu_initialize +{ + char* muhome; + + SCM_ASSERT(scm_is_string(MUHOME) || MUHOME == SCM_BOOL_F || SCM_UNBNDP(MUHOME), + MUHOME, + SCM_ARG1, + FUNC_NAME); + + if (mu_guile_initialized()) + return mu_guile_error(FUNC_NAME, 0, "Already initialized", SCM_UNSPECIFIED); + + if (SCM_UNBNDP(MUHOME) || MUHOME == SCM_BOOL_F) + muhome = NULL; + else + muhome = scm_to_utf8_string(MUHOME); + + if (!mu_guile_init_instance(muhome)) { + free(muhome); + mu_guile_error(FUNC_NAME, 0, "Failed to initialize mu", SCM_UNSPECIFIED); + } + + g_debug("mu-guile: initialized @ %s", muhome ? muhome : ""); + free(muhome); + + /* cleanup when we're exiting */ + atexit(mu_guile_uninit_instance); + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE_PUBLIC(mu_initialized_p, + "mu:initialized?", + 0, + 0, + 0, + (void), + "Whether mu is initialized or not.\n") +#define FUNC_NAME s_mu_initialized_p +{ + return mu_guile_initialized() ? SCM_BOOL_T : SCM_BOOL_F; +} +#undef FUNC_NAME + +SCM_DEFINE(log_func, + "mu:c:log", + 1, + 0, + 1, + (SCM LEVEL, SCM FRM, SCM ARGS), + "log some message at LEVEL using a list of ARGS applied to FRM" + "(in 'simple-format' notation).\n") +#define FUNC_NAME s_log_func +{ + gchar* output; + SCM str; + int level; + + SCM_ASSERT(scm_integer_p(LEVEL), LEVEL, SCM_ARG1, FUNC_NAME); + SCM_ASSERT(scm_is_string(FRM), FRM, SCM_ARG2, ""); + SCM_VALIDATE_REST_ARGUMENT(ARGS); + + level = scm_to_int(LEVEL); + if (level != G_LOG_LEVEL_MESSAGE && level != G_LOG_LEVEL_WARNING && + level != G_LOG_LEVEL_CRITICAL) + return mu_guile_error(FUNC_NAME, 0, "invalid log level", SCM_UNSPECIFIED); + + str = scm_simple_format(SCM_BOOL_F, FRM, ARGS); + + if (!scm_is_string(str)) + return SCM_UNSPECIFIED; + + output = scm_to_utf8_string(str); + g_log(G_LOG_DOMAIN, (GLogLevelFlags)level, "%s", output); + free(output); + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +static struct { + const char* name; + unsigned val; +} VAR_PAIRS[] = { + + {"mu:message", G_LOG_LEVEL_MESSAGE}, + {"mu:warning", G_LOG_LEVEL_WARNING}, + {"mu:critical", G_LOG_LEVEL_CRITICAL}}; + +static void +define_vars(void) +{ + unsigned u; + for (u = 0; u != G_N_ELEMENTS(VAR_PAIRS); ++u) { + scm_c_define(VAR_PAIRS[u].name, scm_from_uint(VAR_PAIRS[u].val)); + scm_c_export(VAR_PAIRS[u].name, NULL); + } +} + +void* +mu_guile_init(void* data) +{ + define_vars(); + +#ifndef SCM_MAGIC_SNARFER +#include "mu-guile.x" +#endif /*SCM_MAGIC_SNARFER*/ + + return NULL; +} diff --git a/guile/mu-guile.hh b/guile/mu-guile.hh new file mode 100644 index 0000000..6265995 --- /dev/null +++ b/guile/mu-guile.hh @@ -0,0 +1,81 @@ +/* +** Copyright (C) 2011-2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_GUILE_H__ +#define __MU_GUILE_H__ + +#include +#include +#include + +/** + * get the singleton Store instance + */ +Mu::Store& mu_guile_store(); + +/** + * whether mu-guile is initialized + * + * @return TRUE if MuGuile is Initialized, FALSE otherwise + */ +gboolean mu_guile_initialized(); + +/** + * raise a guile error (based on a GError) + * + * @param func_name function name + * @param err the error + * + * @return SCM_UNSPECIFIED + */ +SCM mu_guile_g_error(const char* func_name, GError* err); + +/** + * raise a guile error + * + * @param func_name function + * @param status err code + * @param fmt format string for error msg + * @param args params for format string + * + * @return SCM_UNSPECIFIED + */ +SCM mu_guile_error(const char* func_name, int status, const char* fmt, SCM args); + +/** + * convert a string into an SCM -- . It assumes str is in UTF8 encoding, and + * replace characters with '?' if needed. + * + * @param str a string + * + * @return a guile string or #f for empty + */ +SCM mu_guile_scm_from_string(const std::string& str); + +/** + * Initialize this mu guile module. + * + * @param data + * + * @return + */ +extern "C" { +void* mu_guile_init(void* data); +} +#endif /*__MU_GUILE_H__*/ diff --git a/guile/mu-guile.texi b/guile/mu-guile.texi new file mode 100644 index 0000000..ae238b2 --- /dev/null +++ b/guile/mu-guile.texi @@ -0,0 +1,995 @@ +\input texinfo.tex @c -*-texinfo-*- +@c %**start of header +@setfilename mu-guile.info +@settitle mu-guile user manual + +@c Use proper quote and backtick for code sections in PDF output +@c Cf. Texinfo manual 14.2 +@set txicodequoteundirected +@set txicodequotebacktick + +@documentencoding UTF-8 +@c %**end of header + +@include version.texi + +@copying +Copyright @copyright{} 2012 Dirk-Jan C. Binnema + +@quotation +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 or +any later version published by the Free Software Foundation; with no +Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A +copy of the license is included in the section entitled ``GNU Free +Documentation License.'' +@end quotation +@end copying + +@titlepage +@title @t{mu-guile} - extending @t{mu} with Guile Scheme +@subtitle version @value{VERSION} +@author Dirk-Jan C. Binnema + +@c The following two commands start the copyright page. +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@dircategory The Algorithmic Language Scheme +@direntry +* Mu-guile: (mu-guile). Guile-bindings for the mu e-mail indexer/searcher +@end direntry + +@contents + +@ifnottex +@node Top +@top mu-guile manual +@end ifnottex + +@iftex +@node Welcome to mu-guile +@unnumbered Welcome to mu-guile +@end iftex + +Welcome to @t{mu-guile}! + +@t{mu} is a program for indexing and searching your e-mails. It can search +your messages in many different ways, but sometimes that may not be +enough. If you have very specific queries, or want do generate some +statistics, you need some more power. + +@t{mu-guile} is made for those cases. @t{mu-guile} exposes the internals of +@t{mu} and its database to the @t{guile} programming language. Guile is the +@emph{GNU Ubiquitous Intelligent Language for Extensions} - a version of the +@emph{Scheme} programming language and the official GNU extension language. + +Guile/Scheme is a member of the @emph{Lisp} family of programming languages -- +like emacs-lisp, @emph{Racket}, Common Lisp. If you're not familiar with +Scheme, @t{mu-guile} is an excellent opportunity to learn a bit about! + +Trust me, it's not very hard -- and it's @emph{fun}! + +@menu +* Getting started:: +* Initializing mu-guile:: +* Messages:: +* Contacts:: +* Attachments and other parts:: +* Statistics:: +* Plotting data:: +* Writing scripts:: + +Appendices + +* Recipes:: Snippets do specific things +* GNU Free Documentation License:: The license of this manual. +@end menu + +@node Getting started +@chapter Getting started + +@menu +* Installation:: +* Making sure it works:: +@end menu + +This chapter walks you through the installation and the basic setup. + +@node Installation +@section Installation + +@t{mu-guile} is part of @t{mu} - by installing the latter, the former is +necessarily installed as well. At the time of writing, there are no +distribution-provided packaged versions of @t{mu-guile}; so for now, you need +to follow the steps below. + +@subsection Guile 2.x + +@t{mu-guile} is built automatically when @t{mu} is built, if you have +@t{guile} version 2 or higher. (@t{mu} checks for this during +@t{configure}). Thus, the first step is to ensure you have @t{guile} +installed. + +On Debian/Ubuntu you can install @t{guile} 2.x using the @t{guile-2.0-dev} +package (and its dependencies): +@example +$ sudo apt-get install guile-2.0-dev +@end example + +At the time of writing, there are no official packages for +Fedora@footnote{@url{https://bugzilla.redhat.com/show_bug.cgi?id=678238}}. If +you are using Fedora or any other system that does not have packages, you need +to compile @t{guile} from +source@footnote{@url{http://www.gnu.org/software/guile/manual/html_node/Obtaining-and-Installing-Guile.html#Obtaining-and-Installing-Guile}}. + +@subsection gnuplot + +For creating graphs with @t{mu-guile}, you need the @t{gnuplot} program -- +most likely, there is a package available for your system; for example: + +@example +$ sudo apt-get install gnuplot +@end example + +and in Fedora: + +@example +$ sudo yum install gnuplot +@end example + +@subsection mu + +Assuming @t{guile} 2.x is installed correctly, @t{mu} finds it during its +@t{configure}-stage, and creates @t{mu-guile}. Building @t{mu} follows the +normal steps -- please see the @t{mu} documentation for the details. + +The output of @t{./configure} should end with a little text describing the +detected versions of various libraries @t{mu} depends on. In particular, it +should mention the @t{guile} version, e.g. + +@example +Guile version : 2.0.3.82-a2c66 +@end example + +If you don't see any line referring to @t{guile}, please install it, and run +@t{configure} again. After a successful @t{./configure}, we can make and +install the package: + +@example +$ make && sudo make install +@end example + +@subsection mu-guile + +After this, @t{mu} and @t{mu-guile} are installed -- usually somewhere under +@t{/usr/local}.You may need to update @t{guile}'s @code{%load-path} to find it +there. You can check the current @code{%load-path} with the following: + +@example +guile -c '(display %load-path)(newline)' +@end example + +If necessary, you can add the @t{%load-path} by adding to your +@file{~/.guile}: + +@lisp +(set! %load-path (cons "/usr/local/share/guile/site/2.0" %load-path)) +@end lisp + +Or, alternatively, you can set @t{GUILE_LOAD_PATH}: +@example +export GUILE_LOAD_PATH=/usr/local/share/guile/site/2.0 +@end example + +In both cases the directory should be the directory that contains the +installed @t{mu.scm}; if you installed @t{mu} under a different prefix, you +must change the @code{%load-path} accordingly. After this, you should be ready +to go! + +Furthermore, you need to ensure that @t{guile} can find the mu-guile +library; for this we can use @code{LTDL_LIBRARY_PATH}, e.g. +@example +export LTDL_LIBRARY_PATH=/usr/local/lib +@end example + +@node Making sure it works +@section Making sure it works + +Assuming @t{mu-guile} has been installed correctly (@ref{Installation}), and +also assuming that you have already indexed your e-mail messages (if +necessary, see the @t{mu-index} man-page), we are ready to start @t{mu-guile}; +a session may look something like this: + +@cartouche +@verbatim +GNU Guile 2.0.5.123-4bd53 +Copyright (C) 1995-2012 Free Software Foundation, Inc. + +Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. +This program is free software, and you are welcome to redistribute it +under certain conditions; type `,show c' for details. + +Enter `,help' for help. +scheme@(guile-user)> +@end verbatim +@end cartouche + +@noindent +Now, copy-paste the following after the prompt: + +@cartouche +@lisp +(use-modules (mu)) +(mu:initialize) +(for-each + (lambda(msg) + (format #t "Subject: ~a\n" (mu:subject msg))) + (mu:message-list "hello")) +@end lisp +@end cartouche + +@noindent +After pressing @key{Enter}, you should get a list of all subjects of messages +that match @t{hello}: + +@verbatim +... +Subject: RE: The Bird Serpent War Cataclysm +Subject: Hello! +Subject: Re: post-run tomorrow +Subject: When all is lost +... +@end verbatim + +@noindent +If all this works, congratulations! @t{mu-guile} is installed now, ready to +serve your every searching need! + +@node Initializing mu-guile +@chapter Initializing mu-guile + +We now have installed @t{mu-guile}, and in @ref{Making sure it works} +confirmed that things work by trying some simple script. In this and the +following chapters, we take a closer look at programming with @t{mu-guile}. + +It is possible to write separate programs with @t{mu-guile}, but for now we'll +do things @emph{interactively}, that is, from the Guile-prompt +(``@abbr{REPL}''). + +As we have seen, we start our @t{mu-guile} session by starting @t{guile}: + +@verbatim +$ guile +@end verbatim + +@cartouche +@verbatim +GNU Guile 2.0.5.123-4bd53 +Copyright (C) 1995-2012 Free Software Foundation, Inc. + +Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. +This program is free software, and you are welcome to redistribute it +under certain conditions; type `,show c' for details. + +Enter `,help' for help. +scheme@(guile-user)> +@end verbatim +@end cartouche + +The first thing we need to do is loading the modules. All the basics are in +the @t{(mu)} module, with some statistical extras in @t{(mu stats)}, and some +graph plotting functionality in @t{(mu plot)}@footnote{@code{(mu plot)} +requires the @t{gnuplot} program}. Let's load all of them: +@verbatim +scheme@(guile-user)> (use-modules (mu) (mu stats) (mu plot)) +@end verbatim + +The first time you do this, @t{guile} will probably respond by showing some +messages about compiling the modules, and then return to you with another +prompt. Before we can do anything with @t{mu guile}, we need to initialize the +system: + +@verbatim +scheme@(guile-user)> (mu:initialize) +@end verbatim + +This opens the database for reading, using the default location of +@file{~/.cache/mu}@footnote{If you keep your @t{mu} database in a non-standard +place, use @code{(mu:initialize "/path/to/my/mu/")}} + +Now, @t{mu-guile} is ready to go. In the next chapter, we go through the +modules and show what you can do with them. + +@node Messages +@chapter Messages + +In this chapter, we discuss searching messages and doing things with them. + +@menu +* Finding messages:: query for messages in the database +* Message methods:: what methods are available for messages? +* Example - the longest subject:: find the messages with the longest subject +@end menu + +@node Finding messages +@section Finding messages +Now we are ready to retrieve some messages from the system. There are two main +procedures to do this: + +@itemize +@item @code{(mu:message-list [])} +@item @code{(mu:for-each-message [])} +@end itemize + +@noindent +The first procedure, @code{mu:message-list} returns a list of all messages +matching @t{}; if you leave @t{} out, it +returns @emph{all} messages. For example, to get all messages with @t{coffee} +in the subject line: + +@verbatim +scheme@(guile-user)> (mu:message-list "subject:coffee") +$1 = (#< 9040640> #< 9040630> + #< 9040570>) +@end verbatim + +@noindent +Apparently, we have three messages matching @t{subject:coffee}, so we get a +list of three @code{} objects. Let's just use the +@code{mu:subject} procedure ('method') provided by @code{} objects +to retrieve the subject-field (more about methods in the next section). + +For your convenience, @t{guile} has saved the result of our last query in a +variable called @t{$1}, so to get the subject of the first message in the +list, we can do: + +@verbatim +scheme@(guile-user)> (mu:subject (car $1)) +$2 = "Re: best coffee ever!" +@end verbatim + +@noindent +The second procedure we mentioned, @code{mu:for-each-message}, executes some +procedure for each message matched by the search expression (or @emph{all} +messages if the search expression is omitted): + +@verbatim +scheme@(guile-user)> (mu:for-each-message + (lambda(msg) + (display (mu:subject msg)) + (newline)) + "subject:coffee") +Re: best coffee ever! +best coffee ever! +Coffee beans +scheme@(guile-user)> +@end verbatim + +@noindent +Using @code{mu:message-list} and/or +@code{mu:for-each-message}@footnote{Implementation node: +@code{mu:message-list} is implemented in terms of @code{mu:for-each-message}, +not the other way around. Due to the way @t{mu} works, +@code{mu:for-each-message} is rather more efficient than a combination of +@code{for-each} and @code{mu:message-list}} and a couple of @t{} +methods, together with what Guile/Scheme provides, should allow for many +interesting programs. + +@node Message methods +@section Message methods + +Now that we've seen how to retrieve lists of message objects +(@code{}), let's see what we can do with such an object. + +@code{} defines the following methods that all take a single +@code{} object as a parameter. We won't go into the exact meanings +for all of these procedures here - for the details about various flags / +properties, please refer to the @t{mu-find} man-page. + +@itemize +@item @code{(mu:bcc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none +@item @code{(mu:body-html msg)}: : the html body of the message, or @t{#f} if there is none +@item @code{(mu:body-txt msg)}: the plain-text body of the message, or @t{#f} if there is none +@item @code{(mu:cc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none +@item @code{(mu:date msg)}: the @t{Date} field of the message, or 0 if there is none +@item @code{(mu:flags msg)}: list of message-flags for this message +@item @code{(mu:from msg)}: the @t{From} field of the message, or @t{#f} if there is none +@item @code{(mu:maildir msg)}: the maildir this message lives in, or @t{#f} if there is none +@item @code{(mu:message-id msg)}: the @t{Message-Id} field of the message, or @t{#f} if there is none +@item @code{(mu:path msg)}: the file system path for this message +@item @code{(mu:priority msg)}: the priority of this message (either @t{mu:prio:low}, @t{mu:prio:normal} or @t{mu:prio:high} +@item @code{(mu:references msg)}: the list of messages (message-ids) this message +refers to in(mu: the @t{References:} header +@item @code{(mu:size msg)}: size of the message in bytes +@item @code{(mu:subject msg)}: the @t{Subject} field of the message, or @t{#f} if there is none. +@item @code{(mu:tags msg)}: list of tags for this message +@item @code{(mu:timestamp msg)}: the timestamp (mtime) of the message file, or +#f if there is none. +message file +@item @code{(mu:to msg)}: the sender of the message, or @t{#f} if there is none +@end itemize + +With these methods, we can query messages for their properties; for example: + +@verbatim +scheme@(guile-user)> (define msg (car (mu:message-list "snow"))) +scheme@(guile-user)> (mu:subject msg) +$1 = "Re: Running in the snow is beautiful" +scheme@(guile-user)> (mu:flags msg) +$2 = (mu:flag:replied mu:flag:seen) +scheme@(guile-user)> (strftime "%F" (localtime (mu:date msg))) +$3 = "2011-01-15" +@end verbatim + +There are a couple more methods: +@itemize +@item @code{(mu:header msg "")} returns an arbitrary message +header (or @t{#f} if not found) -- e.g. @code{(header msg "User-Agent")} +@item If you include the @t{mu contact} module, the @code{(mu:contacts +msg [contact-type])} method (to get a list of contacts) is +added. @xref{Contacts}. +@item If you include the @t{mu part} module, the @code{((mu:parts msg)} and +@code{(mu:attachments msg)} methods are added. @xref{Attachments and other parts}. +@end itemize + +@node Example - the longest subject +@section Example - the longest subject + +Now, let's write a little example -- let's find out what is the @emph{longest +subject} of any e-mail messages we received in the year 2011. You can try +this if you put the following in a separate file, make it executable, and run +it like any program. + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu)) +(use-modules (srfi srfi-1)) + +(mu:initialize) + +;; note: (subject msg) => #f if there is no subject +(define list-of-subjects + (map (lambda (msg) + (or (mu:subject msg) "")) (mu:message-list "date:2011..2011"))) +;; see the mu-find manpage for the date syntax + +(define longest-subject + (fold (lambda (subj1 subj2) + (if (> (string-length subj1) (string-length subj2)) + subj1 subj2)) + "" list-of-subjects)) + +(format #t "Longest subject: ~s\n" longest-subject) +@end lisp + +There are many other ways to solve the same problem, for example by using an +iterative approach with @code{mu:for-each-message}, but it should show how one +can easily write little programs to answer specific questions about your +e-mail corpus. + +@node Contacts +@chapter Contacts + +We can retrieve the sender and recipients of an e-mail message using methods +like @code{mu:from}, @code{mu:to} etc.; @xref{Message methods}. These +procedures return the list of recipients as a single string; however, often it +is more useful to deal with recipients as separate objects. + +@menu +* Contact procedures and objects:: +* All contacts:: +* Utility procedures:: +* Example - mutt export:: +@end menu + + +@node Contact procedures and objects +@section Contact procedures and objects + +Message objects (@pxref{Messages}) have a method @t{mu:contacts}: + + @code{(mu:contacts [])} + +The @t{} is a symbol, one of @code{mu:to}, @code{mu:from}, +@code{mu:cc} or @code{mu:bcc}. This will then get the contact objects for the +contacts of the corresponding type. If you leave out the contact-type (or +specify @t{#t} for it, you will get a list of @emph{all} contact objects for +the message. + +A contact object (@code{}) has two methods: +@itemize +@item @code{mu:name} returns the name of the contact, or #f if there is none +@item @code{mu:email} returns the e-mail address of the contact, or #f if there is none +@end itemize + +Let's get a list of all names and e-mail addresses in the 'To:' field, of +messages matching 'book': + +@lisp +(use-modules (mu)) +(mu:initialize) +(mu:for-each-message + (lambda (msg) + (for-each + (lambda (contact) + (format #t "~a => ~a\n" + (or (mu:email contact) "") (or (mu:name contact) "no-name"))) + (mu:contacts msg mu:contact:to))) + "book") +@end lisp + +This shows what the methods do, but for many uses, it would be more useful to +have each of the contacts only show up @emph{once} - for that, please refer to +@xref{All contacts}. + +@node All contacts +@section All contacts + +Sometimes you may want to inspect @emph{all} the different contacts in the +@t{mu} database. This is useful, for instance, when exporting contacts to some +external format that can then be important in an e-mail program. + +To enable this, there is the procedure @code{mu:for-each-contact}, defined as + + @code{(mu:for-each-contact procedure [search-expression])}. + +This will aggregate the unique contacts from @emph{all} messages matching +@t{} (when it is left empty, it will match all messages in +the database), and execute @t{procedure} for each of them. + +The @t{procedure} receives an object of the type @t{}, +which is a @emph{subclass} of the @t{} class discussed in +@xref{Contact procedures and objects}. @t{} objects +expose the following additional methods: + +@itemize +@item @code{(mu:frequency )}: returns the @emph{number of times} this contact occurred in +one of the address fields +@item @code{(mu:last-seen )}: returns the @emph{most recent time} the contact was +seen in one of the address fields, as a @t{time_t} value +@end itemize + +The method assumes an e-mail address is unique for a certain contact; if a +certain e-mail address occurs with different names, it uses the most recent +non-empty name. + +@node Utility procedures +@section Utility procedures + +To make dealing with contacts even easier, there are a number of utility +procedures that can save you a bit of typing. + +For converting contacts to some textual form, there is @code{(mu:contact->string + format)}, which takes a contact and returns a text string with +the given format. Currently supported formats are @t{"org-contact}, @t{"mutt-alias"}, +@t{"mutt-ab"}, @t{"wanderlust"} and @t{"plain"}. + + +@node Example - mutt export +@section Example - mutt export + +Let's see how we could export the addresses in the @t{mu} database to the +addressbook format of the venerable +@t{mutt}@footnote{@url{http://www.mutt.org/}} e-mail client. + +The addressbook format that @t{mutt} uses is a sequence of lines that look +something like: +@verbatim +alias [] "<" ">" +@end verbatim + +@t{mu guile} provides the procedure @code{(mu:contact->string +format)} that we can use to do the conversion. + +We may want to focus on people with whom we have frequent correspondence; so +we may want to limit ourselves to people we have seen at least 10 times in the +last year. + +It is a bit hard to @emph{guess} the nick name for e-mail contacts, but +@code{mu:contact->string} tries something based on the name. You can always +adjust them later by hand, obviously. + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu)) +(mu:initialize) + +;; Get a list of contacts that were seen at least 20 times since 2010 +(define (selected-contacts) + (let ((addrs '()) + (start (car (mktime (car (strptime "%F" "2010-01-01"))))) + (minfreq 20)) + (mu:for-each-contact + (lambda (contact) + (if (and (mu:email contact) + (>= (mu:frequency contact) minfreq) + (>= (mu:last-seen contact) start)) + (set! addrs (cons contact addrs))))) + addrs)) + +(for-each + (lambda (contact) + (format #t "~a\n" (mu:contact->string contact "mutt-alias"))) + (selected-contacts)) +@end lisp + +This simple program could be improved in many ways; this is left as an +exercise to the reader. + +@node Attachments and other parts +@chapter Attachments and other parts + +To deal with @emph{attachments}, or, more in general @emph{MIME-parts}, there +is the @t{mu part} module. + +@menu +* Parts and their methods:: +* Attachment example:: +@end menu + +@node Parts and their methods +@section Parts and their methods +The module defines the @code{} class, and adds two methods to +@code{} objects: +@itemize +@item @code{(mu:parts msg)} - returns a list @code{} objects, one for +each MIME-parts in the message. +@item @code{(mu:attachments msg)} - like @code{parts}, but only list those MIME-parts +that look like proper attachments. +@end itemize + +A @code{} object exposes a few methods to get information about the +part: +@itemize +@item @code{(mu:name )} - returns the file name of the mime-part, or @code{#f} if +there is none. +@item @code{(mu:mime-type )} - returns the mime-type of the mime-part, or @code{#f} +if there is none. +@item @code{(mu:size )} - returns the size in bytes of the mime-part +@end itemize + +@c Then, we may want to save the part to a file; this can be done using either: +@c @itemize +@c @item @code{(mu:save part )} - save a part to a temporary file, return the file +@c name@footnote{the temporary filename is a predictable procedure of (user-id, +@c msg-path, part-index)} +@c @item @code{(mu:save-as )} - save part to file at path +@c @end itemize + +@node Attachment example +@section Attachment example + +Let's look at some small example. Let's get a list of the biggest attachments +in messages about Luxemburg: + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu)) +(mu:initialize) + +(define (all-attachments expr) + "Return a list of (name . size) for all attachments in messages +matching EXPR." + (let ((pairs '())) + (mu:for-each-message + (lambda (msg) + (for-each + (lambda (att) ;; add (filename . size) to the list + (set! pairs (cons (cons (mu:name att) (or (mu:size att) 0)) pairs))) + (mu:attachments msg))) + expr) + pairs)) + +(for-each + (lambda (att) + (format #t "~a: ~,1fKb\n" + (car att) (exact->inexact (/ (cdr att) 1024)))) + (sort (all-attachments "Luxemburg") + (lambda (att1 att2) + (< (cdr att1) (cdr att2))))) +@end lisp + +As an exercise for the reader, you might want to re-rewrite the +@code{all-attachments} in terms of @code{mu:message-list}, which would +probably be a bit more elegant. + + +@node Statistics +@chapter Statistics + +@t{mu-guile} offers some convenience procedures to determine various statistics +about the messages in the database. + +@menu +* Basics:: @code{mu:count}, @code{mu:average}, ... +* Tabulating values:: @code{mu:tabulate} +* Most frequent values:: @code{mu:top-n-most-frequent} +@end menu + +@node Basics +@section Basics + +Let's look at some of the basic statistical operations available, in an +interactive session: +@example +GNU Guile 2.0.5.123-4bd53 +Copyright (C) 1995-2012 Free Software Foundation, Inc. + +Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. +This program is free software, and you are welcome to redistribute it +under certain conditions; type `,show c' for details. + +Enter `,help' for help. +scheme@@(guile-user)> ;; load modules, initialize mu +scheme@@(guile-user)> (use-modules (mu) (mu stats)) +scheme@@(guile-user)> (mu:initialize) +scheme@@(guile-user)> +scheme@@(guile-user)> ;; count the number of messages with 'hello' in their subject +scheme@@(guile-user)> (mu:count "subject:hello") +$1 = 162 +scheme@@(guile-user)> ;; average the size of messages with hello in their subject +scheme@@(guile-user)> (mu:average mu:size "subject:hello") +$2 = 34597733/81 +scheme@@(guile-user)> (exact->inexact $2) +$3 = 427132.506172839 +scheme@@(guile-user)> ;; calculate the correlation between message size and +scheme@@(guile-user)> ;; subject length +scheme@@(guile-user)> (mu:correl mu:size (lambda (msg) + (string-length (mu:subject msg))) "subject:hello") +$5 = -0.10804368622292 +scheme@@(guile-user)> +@end example + +@node Tabulating values +@section Tabulating values + +@code{(mu:tabulate [])} applies @t{} to each +message matching @t{} (leave empty to match @emph{all} messages), +and returns a associative list (a list of pairs) with each of the different +results of @t{} and their frequencies. For fields that contain lists +of values (such as address-fields), each of the values in the list is added +separately. + +@subsection Example: messages per weekday + +We demonstrate @code{mu:tabulate} with an example. Suppose we want to know how +many messages we receive per weekday: + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu) (mu stats) (mu plot)) +(mu:initialize) + +;; create a list like (("Sun" . 13) ("Mon" . 23) ...) +(define weekday-table + (mu:weekday-numbers->names + (sort + (mu:tabulate + (lambda (msg) + (tm:wday (localtime (mu:date msg))))) + (lambda (a b) (< (car a) (car b)))))) + +(for-each + (lambda (elm) + (format #t "~a: ~a\n" (car elm) (cdr elm))) + weekday-table) +@end lisp + + +The procedure @code{weekday-table} uses @code{mu:tabulate-message} to get the +frequencies per hour -- this returns a list of pairs: +@verbatim +((5 . 2339) (0 . 2278) (4 . 2800) (2 . 3184) (6 . 1856) (3 . 2833) (1 . 2993)) +@end verbatim + +We sort these pairs by the day number, and then apply +@code{mu:weekday-numbers->names}, which takes the list, and returns a list +where the day numbers are replace by there abbreviated name (in the current +locale). Note, there is also @code{mu:month-numbers->names}. + +The script then outputs these numbers in the following form: + +@verbatim +Sun: 2278 +Mon: 2993 +Tue: 3184 +Wed: 2833 +Thu: 2800 +Fri: 2339 +Sat: 1856 +@end verbatim + +Clearly, Saturday is a slow day for e-mail... + +@node Most frequent values +@section Most frequent values + +In the above example, the number of values is small (the seven weekdays); +however, in many cases there can be many different values (for example, all +different message subjects), many of which may not be very interesting -- all +we need to know is the top-10 of most frequently seen values. + +This is fairly easy to achieve using @code{mu:tabulate} -- to get the top-10 +subjects@footnote{this requires the @code{(srfi srfi-1)}-module}, we can use +something like this: +@lisp +(take + (sort + (mu:tabulate mu:subject) + (lambda (a b) (> (cdr a) (cdr b)))) + 10) +@end lisp + +If this is not short enough, @t{mu-guile} offers a convenience procedure to do +this: @code{mu:top-n-most-frequent}. For example, to get the top-10 people we +sent mail to most often: + +@lisp +(mu:top-n-most-frequent mu:to 10 "maildir:/sent") +@end lisp + +Can't make it much easier than that! + + +@node Plotting data +@chapter Plotting data + +You can plot the results in the format produced by @code{mu:tabulate} with the +@t{(mu plot)} module, an experimental module that requires the +@t{gnuplot}@footnote{@url{http://www.gnuplot.info/}} program to be installed +on your system. + +The @code{mu:plot-histogram} procedure takes the following arguments: + +@code{(mu:plot-histogram <x-label> <y-label> [<want-ascii>])} + +Here, @code{<data>} is a table of data in the format that @code{mu:tabulate} +produces. @code{<title>}, @code{<x-label>} and @code{<y-lablel>} are, +respectively, the title of the graph, and the labels for X- and +Y-axis. Finally, if you pass @t{#t} for the final @code{<want-ascii>} +parameter, a plain-text rendering of the graph will be produced; otherwise, a +graphical window will be shown. + +An example should clarify how this works in practice; let's plot the number of +message per hour: + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu) (mu stats) (mu plot)) +(mu:initialize) + +(define (mail-per-hour-table) + (sort + (mu:tabulate + (lambda (msg) + (tm:hour (localtime (mu:date msg))))) + (lambda (x y) (< (car x) (car y))))) + +(mu:plot-histogram (mail-per-hour-table) "Mail per hour" "Hour" "Frequency") +@end lisp + +@cartouche +@verbatim + Mail per hour +Frequency + 1200 ++--+--+--+--+-+--+--+--+--+-+--+--+--+-+--+--+--+--+-+--+--+--+--++ + |+ + + + + + + "/tmp/fileHz7D2u" using 2:xticlabels(1) ******** + 1100 ++ *** +* + **** * * * + 1000 *+ * **** * +* + * * ****** **** * ** * * + 900 *+ * * ** **** * **** ** * +* + * * * ** * * ********* * ** ** * * + 800 *+ * **** ** * * * * ** * * ** ** * +* + 700 *+ *** **** * ** * * * * ** **** * ** ** * +* + * * * **** * * ** * * * * ** * **** ** ** * * + 600 *+ * **** * * * * ** * * * * ** * * * ** ** * +* + * * ** * * * * * ** * * * * ** * * * ** ** * * + 500 *+ * ** * * * * * ** * * * * ** * * * ** ** * +* + * * ** **** *** * * * ** * * * * ** * * * ** ** * * + 400 *+ * ** ** **** * * * * * ** * * * * ** * * * ** ** * +* + *+ *+**+**+* +*******+* +* +*+ *+**+* +*+ *+ *+**+* +*+ *+**+**+* +* + 300 ******************************************************************** + 0 1 2 3 4 5 6 7 8 910 11 12 1314 15 16 17 1819 20 21 22 23 + Hour +@end verbatim +@end cartouche + +@node Writing scripts +@chapter Writing scripts + +The @t{mu} program has built-in support for running guile-scripts, and comes +with a number of examples. + +You can get a list of all scripts with the @t{mu script} command: +@verbatim +$ mu script +Available scripts (use --verbose for details): + * find-dups: find duplicate messages + * msgs-count: count the number of messages matching some query + * msgs-per-day: graph the number of messages per day + * msgs-per-hour: graph the number of messages per hour + * msgs-per-month: graph the number of messages per month + * msgs-per-year: graph the number of messages per year + * msgs-per-year-month: graph the number of messages per year-month +@end verbatim + +You can then execute such a script by its name: +@verbatim +$ mu msgs-per-month --textonly --query=hello + + + Messages per month matching hello + + 240 ++-+-----+----+-----+-----+-----+----+-----+-----+-----+----+-----+-++ + | + + + + "/tmp/filewi9H0N" using 2:xticlabels(1) ****** | + 220 ++ * * ****** + | * * * * + 200 ++ * * * +* + | * * * * + 180 ++ ****** * * * +* + | * * * * * * + 160 ****** * * * * * +* + * * * * * * * * + * ******* * * * * ****** * * + 140 *+ ** * * * * * * ******** +* + * ** ******* * * * * * ** ** * + 120 *+ ** ** ******* * * * * ** ** +* + * ** ** ** * * * ******* ** ** * + 100 *+ ** ** ** * * * * ** ** ** +* + * + ** + ** + ** + * + * + + * + * + ** + ** + ** + * + 80 ********************************************************************** + Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + Month +@end verbatim + +Please refer to the @t{mu-script} man-page for some details on writing your +own scripts. + + +@node Recipes +@appendix Recipes + +@itemize +@item Calculating the average length of subject-lines +@lisp +;; the average length of all our +(let ((len 0) (n 0)) + (mu:for-each-message + (lambda (msg) + (set! len (+ len (string-length (or (mu:subject msg) "")))) + (set! n (+ n 1)))) + (if (= n 0) 0 (/ len n))) + ;; this gives a rational, exact result; + ;; use exact->inexact to get decimals + +;; we we can make this short with the mu:average (with (mu stats)) +(mu:average (lambda (msg) (string-length (or (mu:subject msg) "")))) + + +@end lisp +@end itemize + +@node GNU Free Documentation License +@appendix GNU Free Documentation License + +@include fdl.texi +@bye diff --git a/guile/mu-guile.x b/guile/mu-guile.x new file mode 100644 index 0000000..8aa8020 --- /dev/null +++ b/guile/mu-guile.x @@ -0,0 +1,4 @@ +/* cpp arguments: mu-guile.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */ +scm_c_define_gsubr (s_mu_initialize, 0, 1, 0, (scm_t_subr) mu_initialize); scm_c_export (s_mu_initialize, __null );; +scm_c_define_gsubr (s_mu_initialized_p, 0, 0, 0, (scm_t_subr) mu_initialized_p); scm_c_export (s_mu_initialized_p, __null );; +scm_c_define_gsubr (s_log_func, 1, 0, 1, (scm_t_subr) log_func);; diff --git a/guile/mu.scm b/guile/mu.scm new file mode 100644 index 0000000..08eae1f --- /dev/null +++ b/guile/mu.scm @@ -0,0 +1,318 @@ +;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(define-module (mu) + :use-module (oop goops) + :use-module (ice-9 optargs) + :use-module (texinfo string-utils) + :export + ( ;; classes + <mu:message> + <mu:contact> + <mu:part> + ;; general +;; mu:initialize + ;; mu:initialized? + mu:log-warning + mu:log-message + mu:log-critical + ;; search funcs + mu:for-each-message + mu:for-each-msg + mu:message-list + ;; message funcs + mu:header + ;; message accessors + mu:field:bcc + mu:field:body-html + mu:field:body-txt + mu:field:cc + mu:field:date + mu:field:flags + mu:field:from + mu:field:maildir + mu:field:message-id + mu:field:path + mu:field:prio + mu:field:refs + mu:field:size + mu:field:subject + mu:field:tags + mu:field:timestamp + mu:field:to + ;; contact funcs + mu:name + mu:email + mu:contact->string + ;; + mu:for-each-contact + ;; + mu:contacts + ;; + ;; <mu:contact-with-stats> + mu:frequency + mu:last-seen + ;; parts + + <mu:part> + ;; message function + mu:attachments + mu:parts + ;; <mu:part> methods + mu:name + mu:mime-type + ;; size + ;; mu:save + ;; mu:save-as + )) + +;; this is needed for guile < 2.0.4 +(setlocale LC_ALL "") + +;; load the binary +(load-extension "libguile-mu" "mu_guile_init") +(load-extension "libguile-mu" "mu_guile_message_init") + +;; define some dummies so we don't get errors during byte compilation +(eval-when (compile) + (define mu:c:get-field) + (define mu:c:get-contacts) + (define mu:c:for-each-message) + (define mu:c:get-header) + (define mu:critical) + (define mu:c:log) + (define mu:message) + (define mu:c:log) + (define mu:warning) + (define mu:c:log) + (define mu:c:get-parts)) + +(define (mu:log-warning frm . args) + "Log FRM with ARGS at warning." + (mu:c:log mu:warning frm args)) + +(define (mu:log-message frm . args) + "Log FRM with ARGS at warning." + (mu:c:log mu:message frm args)) + +(define (mu:log-critical frm . args) + "Log FRM with ARGS at warning." + (mu:c:log mu:critical frm args)) + +(define-class <mu:message> () + (msg #:init-keyword #:msg)) ;; the MuMsg-smob we're wrapping + +(define-syntax define-getter + (syntax-rules () + ((define-getter method-name field) + (begin + (define-method (method-name (msg <mu:message>)) + (mu:c:get-field (slot-ref msg 'msg) field)) + (export method-name))))) + +(define-getter mu:bcc mu:field:bcc) +(define-getter mu:body-html mu:field:body-html) +(define-getter mu:body-txt mu:field:body-txt) +(define-getter mu:cc mu:field:cc) +(define-getter mu:date mu:field:date) +(define-getter mu:flags mu:field:flags) +(define-getter mu:from mu:field:from) +(define-getter mu:maildir mu:field:maildir) +(define-getter mu:message-id mu:field:message-id) +(define-getter mu:path mu:field:path) +(define-getter mu:priority mu:field:prio) +(define-getter mu:references mu:field:refs) +(define-getter mu:size mu:field:size) +(define-getter mu:subject mu:field:subject) +(define-getter mu:tags mu:field:tags) +(define-getter mu:timestamp mu:field:timestamp) +(define-getter mu:to mu:field:to) + +(define-method (mu:header (msg <mu:message>) (hdr <string>)) + "Get an arbitrary header HDR from message MSG; return #f if it does +not exist." + (mu:c:get-header (slot-ref msg 'msg) hdr)) + +(define* (mu:for-each-message func #:optional (expr #t) (maxresults -1)) + "Execute function FUNC for each message that matches mu search expression EXPR. +If EXPR is not provided, match /all/ messages in the store. MAXRESULTS +specifies the maximum of messages to return, or -1 (the default) for +no limit." + (mu:c:for-each-message + (lambda (msg) + (func (make <mu:message> #:msg msg))) + expr + maxresults)) + +;; backward-compatibility alias +(define mu:for-each-msg mu:for-each-message) + +(define* (mu:message-list #:optional (expr #t) (maxresults -1)) + "Return a list of all messages matching mu search expression +EXPR. If EXPR is not provided, return a list of /all/ messages in the +store. MAXRESULTS specifies the maximum of messages to return, or +-1 (the default) for no limit." + (let ((lst '())) + (mu:for-each-message + (lambda (m) + (set! lst (append! lst (list m)))) expr maxresults) + lst)) + +;; contacts +(define-class <mu:contact> () + (name #:init-value #f #:accessor mu:name #:init-keyword #:name) + (email #:init-value #f #:accessor mu:email #:init-keyword #:email)) + +(define-method (mu:contacts (msg <mu:message>) contact-type) + "Get all contacts for MSG of the given CONTACT-TYPE. MSG is of type <mu-message>, +while contact type is either `mu:contact:to', `mu:contact:cc', +`mu:contact:from' or `mu:contact:bcc' to get the corresponding type of +contacts, or #t to get all. + +Returns a list of <mu-contact> objects." + (map (lambda (pair) ;; a pair (na . addr) + (make <mu:contact> #:name (car pair) #:email (cdr pair))) + (mu:c:get-contacts (slot-ref msg 'msg) contact-type))) + +(define-method (mu:contacts (msg <mu:message>)) + "Get contacts of all types for message MSG as a list of <mu-contact> +objects." + (mu:contacts msg #t)) + +(define-class <mu:contact-with-stats> (<mu:contact>) + (tstamp #:init-value 0 #:accessor mu:timestamp #:init-keyword #:timestamp) + (last-seen #:init-value 0 #:accessor mu:last-seen) + (freq #:init-value 1 #:accessor mu:frequency)) + +(define* (mu:for-each-contact proc #:optional (expr #t)) + "Execute PROC for each contact. PROC receives a <mu-contact> instance +as parameter. If EXPR is specified, only consider contacts in messages +matching EXPR." + (let ((c-hash (make-hash-table 4096))) + (mu:for-each-message + (lambda (msg) + (for-each + (lambda (ct) + (let ((ct-ws (make <mu:contact-with-stats> + #:name (mu:name ct) + #:email (mu:email ct) + #:timestamp (mu:date msg)))) + (update-contacts-hash c-hash ct-ws))) + (mu:contacts msg #t))) + expr) + (hash-for-each ;; c-hash now contains a map of email->contact + (lambda (email ct-ws) (proc ct-ws)) c-hash))) + +(define-method (update-contacts-hash c-hash (nc <mu:contact-with-stats>)) + "Update the contacts hash with a new and/or existing contact." + ;; xc: existing-contact, nc: new contact + (let ((xc (hash-ref c-hash (mu:email nc)))) + (if (not xc) ;; no existing contact with this email address? + (hash-set! c-hash (mu:email nc) nc) ;; store the new contact. + ;; otherwise: + (begin + ;; 1) update the frequency for the existing contact + (set! (mu:frequency xc) (1+ (mu:frequency xc))) + ;; 2) update the name if the new one is not empty and its timestamp is newer + ;; in that case, also update the timestamp + (if (and (mu:name nc) (> (string-length (mu:name nc))) + (> (mu:timestamp nc) (mu:timestamp xc))) + (set! (mu:name xc) (mu:name nc)) + (set! (mu:timestamp xc) (mu:timestamp nc))) + ;; 3) update last-seen with timestamp, if x's timestamp is newer + (if (> (mu:timestamp nc) (mu:last-seen xc)) + (set! (mu:last-seen xc) (mu:timestamp nc))) + ;; okay --> now xc has been updated; but it back in the hash + (hash-set! c-hash (mu:email xc) xc))))) + +(define-method (mu:contact->string (contact <mu:contact>) (form <string>)) + "Convert a contact to a string in format FORM, which is a string, +either \"org-contact\", \"mutt-alias\", \"mutt-ab\", +\"wanderlust\", \"quoted\" \"plain\"." + (let* ((name (mu:name contact)) (email (mu:email contact)) + (nick ;; simplistic nick guessing... + (string-map + (lambda(kar) + (if (char-alphabetic? kar) kar #\_)) + (string-downcase (or name email))))) + (cond + ((string= form "plain") + (format #f "~a~a~a" (or name "") (if name " " "") email)) + ((string= form "org-contact") + (format #f "* ~s\n:PROPERTIES:\n:EMAIL:~a\n:NICK:~a\n:END:" + (or name email) email nick)) + ((string= form "wanderlust") + (format #f "~a ~s ~s" + nick (or name email) email)) + ((string= form "mutt-alias") + (format #f "alias ~a ~a <~a>" + nick (or name email) email)) + ((string= form "mutt-ab") + (format #f "~a\t~a\t" + email (or name ""))) + ((string= form "quoted") + (string-append + "\"" + (escape-special-chars + (string-append + (if name + (format #f "\"~a\" " name) + "") + (format #f "<~a>" email)) + "\"" #\\) + "\"")) + (else (error "Unsupported format"))))) + +;; message parts + + +(define-class <mu:part> () + (msgpath #:init-value #f #:init-keyword #:msgpath) + (index #:init-value #f #:init-keyword #:index) + (name #:init-value #f #:getter mu:name #:init-keyword #:name) + (mime-type #:init-value #f #:getter mu:mime-type #:init-keyword #:mime-type) + (size #:init-value 0 #:getter mu:size #:init-keyword #:size)) + +(define-method (get-parts (msg <mu:message>) (files-only <boolean>)) + "Get the part for MSG as a list of <mu:part> objects; if FILES-ONLY is #t, +only get the part with file names." + (map (lambda (part) + (make <mu:part> + #:msgpath (list-ref part 0) + #:index (list-ref part 1) + #:name (list-ref part 2) + #:mime-type (list-ref part 3) + #:size (list-ref part 4))) + (mu:c:get-parts (slot-ref msg 'msg) files-only))) + +(define-method (mu:attachments (msg <mu:message>)) + "Get the attachments for MSG as a list of <mu:part> objects." + (get-parts msg #t)) + +(define-method (mu:parts (msg <mu:message>)) + "Get the MIME-parts for MSG as a list of <mu-part> objects." + (get-parts msg #f)) + +;; (define-method (mu:save (part <mu:part>)) +;; "Save PART to a temporary file, and return the file name. If the +;; part had a filename, the temporary file's file name will be just that; +;; otherwise a name is made up." +;; (mu:save-part (slot-ref part 'msgpath) (slot-ref part 'index))) + +;; (define-method (mu:save-as (part <mu:part>) (filepath <string>)) +;; "Save message-part PART to file system path PATH." +;; (copy-file (save part) filepath)) diff --git a/guile/mu/Makefile.am b/guile/mu/Makefile.am new file mode 100644 index 0000000..9339ad9 --- /dev/null +++ b/guile/mu/Makefile.am @@ -0,0 +1,26 @@ +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +scmdir=${prefix}/share/guile/site/${GUILE_EFFECTIVE_VERSION}/mu/ + +scm_DATA= \ + stats.scm \ + plot.scm \ + script.scm + +EXTRA_DIST=$(scm_DATA) diff --git a/guile/mu/README b/guile/mu/README new file mode 100644 index 0000000..634ad8b --- /dev/null +++ b/guile/mu/README @@ -0,0 +1,207 @@ +* OUTDATED * + +* README + +** What is muile? + + `muile' is a little experiment/toy using the equally experimental mu guile + bindings, to be found in libmuguile/ in the top-level source directory. + + `guile'[1] is an interpreter/library for the Scheme programming language[2], + specifically meant for extending other programs. It is, in fact, the + official GNU language for doing so. 'muile' requires guile 2.x to get the full + support. + + Older versions may not support e.g. the 'mu-stats.scm' things discussed below. + + The combination of mu + guile is called `muile', and allows you to write + little Scheme-programs to query the mu-database, or inspect individual + messages. It is still in an experimental stage, but useful already. + +** How do I get it? + + The git-version and the future 0.9.7 version of mu will automatically build + muile if you have guile. I've been using guile 2.x from git, but installing + the 'guile-1.8-dev' package (Ubuntu/Debian) should do the trick. (I only did + very minimal testing with guile 1.8 though). + + Then, configure mu. The configure output should tell you about whether guile + was found (and where). If it's found, build mu, and toys/muile should be + created, as well. + +** What can I do with it? + + Go to toys/muile and start muile. You'll end up with a guile-shell where you + can type scheme [1], it looks something like this (for guile 2.x): + + ,---- + | scheme@(guile-user)> + `---- + + Now, let's load a message (of course, replace with a message on your system): + + ,---- + | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S")) + `---- + + This defines a variable 'msg', which holds some message on your file + system. It's now easy to inspect this message: + + ,---- + | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S")) + `---- + + Now, we can inspect this message a bit: + ,---- + | scheme@(guile-user)> (mu:msg:subject msg) + | $1 = "See me in bikini :-)" + | scheme@(guile-user)> (mu:msg:flags msg) + | $2 = (mu:attach mu:unread) + `---- + + and so on. Note, it's probably easiest to explore the various mu: methods + using autocompletion; to enable that make sure you have + + + ,---- + | (use-modules (ice-9 readline)) + | (activate-readline) + `---- + + in your ~/.guile configuration. + +** does this tool have some parameters? + + Yes, there is --muhome to set a non-default place for the message database + (see the documentation on --muhome in the mu-find manpage). + + And there is --msg=<path> where you specify some particular message file; + it will be available as 'mu:current-msg' in the guile (muile) environment. For + example: + + ,---- + | ./muile --msg=~/Maildir/inbox/cur/1311310172_1234:2,S + | [...] + | scheme@(guile-user)> mu:current-msg + | $1 = #<msg /home/djcb/Maildir/inbox/cur/1311310172_1234:2,S> + | scheme@(guile-user)> (mu:msg:size mu:current-msg) + | $2 = 7206 + `---- + +** What about searching messages in the database? + + That's easy, too - it does require a little more scheme knowledge. For + searching messages there is the mu:store:for-each function, which takes two + arguments; the first argument is a function that will be called for each + message found. The optional second argument is the search expression (following + 'mu find' syntax); if don't provide the argument, all messages match. + + So how does this work in practice? Let's see I want to see the subject and + sender for messages about milk: + + ,---- + | (mu:store:for-each (lambda(msg) (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg))) "milk") + `---- + + or slightly more readable: + + ,---- + | (mu:store:for-each + | (lambda(msg) + | (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg))) + | "milk") + `---- + + As you can see, I provide an anonymous ('lambda') function which will be + called for each message matching 'milk'. Admittedly, this requires a bit of + Scheme-knowledge... but this time is good as any to learn this nice + language. + + +** Can I do some statistics on my messages? + + Yes you can. In fact, it's pretty easy. If you load (in the muile/ directory) + the file 'mu-stats.scm': + + ,---- + | (load "mu-stats.scm") + `---- + + you'll get a bunch of functions (with names starting with 'mu:stats') to make + this very easy. Let's see, suppose I want to see how many messages I get per + weekday: + + ,---- + | scheme@(guile-user)> (mu:stats:per-weekday) + | $1 = ((0 . 2255) (1 . 2788) (2 . 2868) (3 . 2599) (4 . 2629) (5 . 2287) (6 . 1851)) + `---- + + Note, Sunday=0, Monday=1 and so on. Apparently, I get/send most of e-mail on + Tuesdays, and least on Saturday. + + And note that mu:stats:per-weekdays takes an optional search expression + argument, to limit the results to messages matching that, e.g., to only + consider messages related to emacs during this year: + + ,---- + | scheme@(guile-user)> (mu:stats:per-weekday "emacs date:2011..now") + | $8 = ((0 . 54) (1 . 22) (2 . 46) (3 . 47) (4 . 39) (5 . 54) (6 . 50)) + `---- + + There's also 'mu:stats:per-month', 'mu:stats:per-year', 'mu:stats:per-hour'. + I learnt that during 3-4am I sent/receive only about a third of what I sent + during 11-12pm. + +** What about getting the top-10 people in the To:-field? + + Easy. + + ,---- + | scheme@(guile-user)> (mu:stats:top-n-to) + | $1 = ((("Abc" "myself@example.com") . 4465) (("Def" "somebodyelse@example.com") . 2114) + | (and so on) + `---- + + I've changed the names a bit to protect the innocent, but what the function + does is return a list of pairs of + + (<name> <email>) . <frequency> + + descending in order of frequency. Note, 'mu:stats:top-n-to' takes two + optional arguments - first the 'n' in top-n (default is 10), and seconds as + search expression to limit the messages considered. + + There are also the functions 'mu:stats:top-n-subject' and + 'mu:stats:top-n-from' which do the same, mutatis mutandis, and it's quite + easy to add your own (see the mu-stats.scm for examples) + +** What about showing the results in a table? + + Even easier. Try: + + ,---- + | (mu:stats:table (mu:stats:top-n-to)) + `---- + + or + + ,---- + | (mu:stats:table (mu:stats:per-weekday)) + `---- + + You can also export the table: + + ,---- + | (mu:stats:export (mu:stats:per-weekday)) + `---- + + which will create a temporary file with the results, for further processing + in e.g. 'R' or 'gnuplot'. + + +[1] http://www.gnu.org/s/guile/ +[2] http://en.wikipedia.org/wiki/Scheme_(programming_language) + +# Local Variables: +# mode: org; org-startup-folded: nil +# End: diff --git a/guile/mu/contact.scm b/guile/mu/contact.scm new file mode 100644 index 0000000..843d9c4 --- /dev/null +++ b/guile/mu/contact.scm @@ -0,0 +1,4 @@ +(define-module (mu contact) :use-module(mu)) +(display "(mu contact) is deprecated, please remove from (use-modules ...)") +(newline) + diff --git a/guile/mu/message.scm b/guile/mu/message.scm new file mode 100644 index 0000000..bc9b27a --- /dev/null +++ b/guile/mu/message.scm @@ -0,0 +1,4 @@ +(define-module (mu message) :use-module (mu)) +(display "(mu message) is deprecated, please remove from (use-modules ...)") +(newline) + diff --git a/guile/mu/part.scm b/guile/mu/part.scm new file mode 100644 index 0000000..f9b9cd3 --- /dev/null +++ b/guile/mu/part.scm @@ -0,0 +1,4 @@ +(define-module (mu part) :use-module (mu)) +(display "(mu part) is deprecated, please remove from (use-modules ...)") +(newline) + diff --git a/guile/mu/plot.scm b/guile/mu/plot.scm new file mode 100644 index 0000000..adeb80f --- /dev/null +++ b/guile/mu/plot.scm @@ -0,0 +1,80 @@ +;; +;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(define-module (mu plot) + :use-module (mu) + :use-module (ice-9 popen) + :export ( mu:plot ;; alias for mu:plot-histogram + mu:plot-histogram + )) + +(define (export-pairs pairs) + "Write a temporary file with the list of PAIRS in table format, and +return the file name." + (let* ((datafile (tmpnam)) + (output (open datafile (logior O_CREAT O_WRONLY) #O0600))) + (for-each + (lambda(pair) + (display (format #f "~a ~a\n" (car pair) (cdr pair)) output)) + pairs) + (close output) + datafile)) + +(define (find-program-in-path prog) + "Find exutable program PROG in PATH; return the full path, or #f if +not found." + (let* ((path (parse-path (getenv "PATH"))) + (progpath (search-path path prog))) + (if (not progpath) + #f + (if (access? progpath X_OK) ;; is + progpath + #f)))) + +(define* (mu:plot-histogram data title x-label y-label + #:optional (output "dumb") (extra-gnuplot-opts '())) + "Plot DATA with TITLE, X-LABEL and X-LABEL using the gnuplot +program. DATA is a list of cons-pairs (X . Y). + + OUTPUT is a string +that determines the type of output that gnuplot produces, depending on +the system. Which options are available depends on the particulars for +the gnuplot installation, but typical examples would be \"dumb\" for +text-only display, \"wxterm\" to write to a graphical window, or +\"png\" to write a PNG-image to stdout. + +EXTRA-GNUPLOT-OPTS is a list +of any additional options for gnuplot." + (if (not (find-program-in-path "gnuplot")) + (error "cannot find 'gnuplot' in path")) + (let ((datafile (export-pairs data)) + (gnuplot (open-pipe "gnuplot -p" OPEN_WRITE))) + (display (string-append + "reset\n" + "set term " (or output "dumb") "\n" + "set title \"" title "\"\n" + "set xlabel \"" x-label "\"\n" + "set ylabel \"" y-label "\"\n" + "set boxwidth 0.9\n" + (string-join extra-gnuplot-opts "\n") + "plot \"" datafile "\" using 2:xticlabels(1) with boxes fs solid\n") + gnuplot) + (close-pipe gnuplot))) + +;; backward compatibility +(define mu:plot mu:plot-histogram) diff --git a/guile/mu/script.scm b/guile/mu/script.scm new file mode 100644 index 0000000..3a62948 --- /dev/null +++ b/guile/mu/script.scm @@ -0,0 +1,57 @@ +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +(define-module (mu script) + :export (mu:run-stats)) + +(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format)) +(use-modules (mu) (mu stats) (mu plot)) + +(define (help-and-exit) + "Show some help." + (display + (string-append "usage: script [--help] [--textonly] " + "[--muhome=<muhome>] [--query=<query>") + (newline)) + (exit 0)) + +(define (mu:run-stats args func) + "Run some statistics function. +Interpret argument-list ARGS (like command-line +arguments). Possible arguments are: + --help (show some help and exit) + --muhome (path to alternative mu home directory) + --output (a string describing the output, e.g. \"dumb\", \"png\" \"wxt\") + searchexpr (a search query) +then call FUNC with args SEARCHEXPR and OUTPUT." + (setlocale LC_ALL "") + (let* ((optionspec '((muhome (value #t)) + (query (value #t)) + (output (value #f)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (query (option-ref options 'query #f)) + (help (option-ref options 'help #f)) + (output (option-ref options 'output #f)) + (muhome (option-ref options 'muhome #f)) + (restargs (option-ref options '() #f))) + (if help (help-and-exit)) + (mu:initialize muhome) + (func (or query "") output))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/mu/stats.scm b/guile/mu/stats.scm new file mode 100644 index 0000000..90ab836 --- /dev/null +++ b/guile/mu/stats.scm @@ -0,0 +1,165 @@ +;; +;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(define-module (mu stats) + :use-module (oop goops) + :use-module (mu) + :use-module (srfi srfi-1) + :use-module (ice-9 i18n) + :use-module (ice-9 r5rs) + :export ( mu:tabulate + mu:top-n-most-frequent + mu:count + mu:average + mu:stddev + mu:correl + mu:max + mu:min + mu:weekday-numbers->names + mu:month-numbers->names)) + + +(define* (mu:tabulate func #:optional (expr #t)) + "Execute FUNC for each message matching EXPR, and return an alist +with maps each result of FUNC to its frequency. If the result of FUNC +is a list, add each of its values separately. + FUNC is a function takes a <mu-message> instance as its argument. For +example, to tabulate messages by weekday, one could use: + (mu:tabulate (lambda(msg) (tm:wday (localtime (date msg))))), and +get back a list like + ((1 . 2) (2 . 5)(3 . 4)(4 . 4)(5 . 12)(6 . 7)(7. 2))." + (let* ((table '()) + ;; func to add a value to our table + (update-table + (lambda (val) + (let ((old-freq (or (assoc-ref table val) 0))) + (set! table (assoc-set! table val (1+ old-freq))))))) + (mu:for-each-message + (lambda(msg) + (let ((val (func msg))) + (if (list? val) + (for-each update-table val) + (update-table val)))) + expr) + table)) + +(define* (top-n func less n #:optional (expr #t)) + "Take the results of (mu:tabulate FUNC EXPR), sort using LESS (a +function taking two arguments A and B (cons cells, (VAL . FREQ)), and +returns #t if A < B, #f otherwise), and then take the first N." + (take (sort (mu:tabulate func expr) less) n)) + +(define* (mu:top-n-most-frequent func n #:optional (expr #t)) + "Take the results of (mu:tabulate FUNC EXPR), and return the N items with the highest frequency." + (top-n func (lambda (a b) (> (cdr a) (cdr b))) n expr)) + +(define* (mu:count #:optional (expr #t)) + "Count the number of messages matching EXPR. If EXPR is not +provided, match /all/ messages." + (let ((num 0)) + (mu:for-each-message + (lambda (msg) (set! num (1+ num))) + expr) + num)) + +(define (average lst) + "Calculate the average of a list LST of numbers, or #f if undefined." + (if (null? lst) + #f + (/ (apply + lst) (length lst)))) + +(define (stddev lst) + "Calculate the standard deviation of a list LST of numbers or #f if +undefined." + (let* ((avg (average lst)) + (sosq (if avg + (apply + (map (lambda (x)(* (- x avg) (- x avg))) lst))))) + (if sosq + (sqrt (/ sosq (length lst)))))) + + +(define* (mu:average func #:optional (expr #t)) + "Get the average value of FUNC applied to all messages matching +EXPR (or #t for all). Returns #f if undefined." + (average (map func (mu:message-list expr)))) + +(define* (mu:stddev func #:optional (expr #t)) + "Get the standard deviation the the values of FUNC applied to all +messages matching EXPR (or #t for all). This is the 'population' stddev, not the 'sample' stddev. Returns #f if undefined." + (stddev (map func (mu:message-list expr)))) + +(define* (mu:max func #:optional (expr #t)) + "Get the maximum value of FUNC applied to all messages matching +EXPR (or #t for all). Returns #f if undefined." + (apply max (map func (mu:message-list expr)))) + +(define* (mu:min func #:optional (expr #t)) + "Get the minimum value of FUNC applied to all messages matching +EXPR (or #t for all). Returns #f if undefined." + (apply min (map func (mu:message-list expr)))) + + +(define (correl lst) + "Calculate Pearson's correlation coefficient for a list LST of cons +pair, where the car and cdr of the pairs are values from data set 1 +and 2, respectively." + (let ((n (length lst)) + (sx (apply + (map car lst))) + (sy (apply + (map cdr lst))) + (sxy (apply + (map (lambda (cell) (* (car cell) (cdr cell))) lst))) + (sxx (apply + (map (lambda (cell) (* (car cell) (car cell))) lst))) + (syy (apply + (map (lambda (cell) (* (cdr cell) (cdr cell))) lst)))) + (/ (- (* n sxy) (* sx sy)) + (sqrt (* (- (* n sxx) (* sx sx)) (- (* n syy) (* sy sy))))))) + +(define* (mu:correl func1 func2 #:optional (expr #t)) + "Determine Pearson's correlation coefficient between the value for +functions FUNC1 and FUNC2 to all messages matching EXPR (or #t for +all). Returns #f if undefined." + (let ((data + (map (lambda (msg) + (cons (func1 msg) (func2 msg))) + (mu:message-list expr)))) + (if data (correl data) #f))) + + +;; a list of abbreviated, localized day names +(define day-names + (map locale-day-short (iota 7 1))) + +(define (mu:weekday-numbers->names table) + "Convert a list of pairs with the car denoting a day number (0-6) +into a list of pairs with the car replaced by the corresponding day +name (abbreviated) for the current locale." + (map + (lambda (pair) + (cons (list-ref day-names (car pair)) (cdr pair))) + table)) + +;; a list of abbreviated, localized month names +(define month-names + (map locale-month-short (iota 12 1))) + +(define (mu:month-numbers->names table) + "Convert a list of pairs with the car denoting a month number (0-11) +into a list of pairs with the car replaced by the corresponding day +name (abbreviated)." + (map + (lambda (pair) + (cons (list-ref month-names (car pair)) (cdr pair))) + table)) diff --git a/guile/scripts/Makefile.am b/guile/scripts/Makefile.am new file mode 100644 index 0000000..c846596 --- /dev/null +++ b/guile/scripts/Makefile.am @@ -0,0 +1,29 @@ +## Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +EXTRA_DIST= \ + msgs-count.scm \ + msgs-per-year.scm \ + msgs-per-hour.scm \ + msgs-per-month.scm \ + msgs-per-day.scm \ + msgs-per-year-month.scm \ + find-dups.scm + +muguiledistscriptdir = $(pkgdatadir)/scripts/ +muguiledistscript_SCRIPTS = $(EXTRA_DIST) diff --git a/guile/scripts/find-dups.scm b/guile/scripts/find-dups.scm new file mode 100755 index 0000000..778acfe --- /dev/null +++ b/guile/scripts/find-dups.scm @@ -0,0 +1,119 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2013-2015 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: find duplicate messages +;; INFO: options: +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --delete: delete all but the first one (experimental, be careful!) + +(use-modules (mu) (mu script) (mu stats)) +(use-modules (ice-9 getopt-long) (ice-9 optargs) + (ice-9 popen) (ice-9 format) (ice-9 rdelim)) + +(define (md5sum path) + (let* ((port (open-pipe* OPEN_READ "md5sum" path)) + (md5 (read-delimited " " port))) + (close-pipe port) + md5)) + +(define (find-dups delete expr) + (let ((id-table (make-hash-table 20000))) + ;; fill the hash with <msgid-size> => <list of paths> + (mu:for-each-message + (lambda (msg) + (let* ((id (format #f "~a-~d" (mu:message-id msg) + (mu:size msg))) + (lst (hash-ref id-table id))) + (if lst + (set! lst (cons (mu:path msg) lst)) + (set! lst (list (mu:path msg)))) + (hash-set! id-table id lst))) + expr) + ;; list all the paths with multiple elements; check the md5sum to + ;; make 100%-minus-ε sure they are really the same file. + (hash-for-each + (lambda (id paths) + (if (> (length paths) 1) + (let ((hash (make-hash-table 10))) + (for-each + (lambda (path) + (when (file-exists? path) + (let* ((md5 (md5sum path)) (lst (hash-ref hash md5))) + (if lst + (set! lst (cons path lst)) + (set! lst (list path))) + (hash-set! hash md5 lst)))) + paths) + ;; hash now maps the md5sum to the messages... + (hash-for-each + (lambda (md5 mpaths) + (if (> (length mpaths) 1) + (begin + ;;(format #t "md5sum: ~a:\n" md5) + (let ((num 1)) + (for-each + (lambda (path) + (if (equal? num 1) + (format #t "~a\n" path) + (begin + (format #t "~a: ~a\n" (if delete "deleting" "dup") path) + (if delete (delete-file path)))) + (set! num (+ 1 num))) + mpaths))))) + hash)))) + id-table))) + + + +(define (main args) + "Find duplicate messages and, potentially, delete the dups. + Be careful with that! +Interpret argument-list ARGS (like command-line +arguments). Possible arguments are: + --muhome (path to alternative mu home directory). + --delete (delete all but the first one). Run mu index afterwards. + --expr (expression to constrain search)." + (setlocale LC_ALL "") + (let* ((optionspec '( (muhome (value #t)) + (delete (value #f)) + (expr (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (help (option-ref options 'help #f)) + (delete (option-ref options 'delete #f)) + (expr (option-ref options 'expr #t)) + (muhome (option-ref options 'muhome #f))) + (mu:initialize muhome) + (find-dups delete expr))) + + +;; Local Variables: +;; mode: scheme +;; End: + + + + + + + + + diff --git a/guile/scripts/msgs-count.scm b/guile/scripts/msgs-count.scm new file mode 100755 index 0000000..923e3a5 --- /dev/null +++ b/guile/scripts/msgs-count.scm @@ -0,0 +1,40 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir + +(use-modules (mu) (mu script) (mu stats)) + +(define (count expr output) + "Print the total number of messages matching the query EXPR. +OUTPUT is ignored." + (display (mu:count expr)) + (newline)) + +(define (main args) + (mu:run-stats args count)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-day.scm b/guile/scripts/msgs-per-day.scm new file mode 100755 index 0000000..824f556 --- /dev/null +++ b/guile/scripts/msgs-per-day.scm @@ -0,0 +1,49 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-day expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (mu:weekday-numbers->names + (sort (mu:tabulate + (lambda (msg) + (tm:wday (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per weekday matching ~a" expr) + "Day" "Messages" output)) + +(define (main args) + (mu:run-stats args per-day)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-hour.scm b/guile/scripts/msgs-per-hour.scm new file mode 100755 index 0000000..d96f9b6 --- /dev/null +++ b/guile/scripts/msgs-per-hour.scm @@ -0,0 +1,49 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-hour expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (sort + (mu:tabulate + (lambda (msg) + (tm:hour (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per hour matching ~a" expr) + "Hour" "Messages" output)) + +(define (main args) + (mu:run-stats args per-hour)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-month.scm b/guile/scripts/msgs-per-month.scm new file mode 100755 index 0000000..50dbbed --- /dev/null +++ b/guile/scripts/msgs-per-month.scm @@ -0,0 +1,50 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-month expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (mu:month-numbers->names + (sort + (mu:tabulate + (lambda (msg) + (tm:mon (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per month matching ~a" expr) + "Month" "Messages" output)) + +(define (main args) + (mu:run-stats args per-month)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-year-month.scm b/guile/scripts/msgs-per-year-month.scm new file mode 100755 index 0000000..33b1447 --- /dev/null +++ b/guile/scripts/msgs-per-year-month.scm @@ -0,0 +1,52 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-year-month expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (sort (mu:tabulate + (lambda (msg) + (string->number + (format #f "~d~2'0d" + (+ 1900 (tm:year (localtime (mu:date msg)))) + (tm:mon (localtime (mu:date msg)))))) + expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year/month matching ~a" expr) + "Year/Month" "Messages" output)) + +(define (main args) + (mu:run-stats args per-year-month)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-year.scm b/guile/scripts/msgs-per-year.scm new file mode 100755 index 0000000..242e299 --- /dev/null +++ b/guile/scripts/msgs-per-year.scm @@ -0,0 +1,48 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-year expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (sort (mu:tabulate + (lambda (msg) + (+ 1900 (tm:year (localtime (mu:date msg))))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year matching ~a" expr) + "Year" "Messages" output)) + +(define (main args) + (mu:run-stats args per-year)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/tests/meson.build b/guile/tests/meson.build new file mode 100644 index 0000000..dc89051 --- /dev/null +++ b/guile/tests/meson.build @@ -0,0 +1,31 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +guile_load_path=':'.join([ # meson 0.56 has project_source_root + join_paths(meson.source_root(), 'guile'), + join_paths(meson.current_build_dir(), '..')]) + +test('test-mu-guile', + executable('test-mu-guile', + 'test-mu-guile.cc', + install: false, + cpp_args: [ + '-DABS_SRCDIR="' + meson.current_source_dir() + '"', + '-DGUILE_LOAD_PATH="' + guile_load_path + '"', + '-DGUILE_EXTENSIONS_PATH="' + guile_load_path + '"' + ], + dependencies: [glib_dep, lib_mu_dep])) diff --git a/guile/tests/test-mu-guile.cc b/guile/tests/test-mu-guile.cc new file mode 100644 index 0000000..a630d16 --- /dev/null +++ b/guile/tests/test-mu-guile.cc @@ -0,0 +1,131 @@ +/* +** Copyright (C) 2012-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include <glib.h> +#include <glib/gstdio.h> + +#include <lib/mu-query.hh> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> + +#include "utils/mu-test-utils.hh" +#include <lib/mu-store.hh> +#include <utils/mu-utils.hh> + +using namespace Mu; + +static std::string test_dir; + +static std::string +fill_database(void) +{ + const auto cmdline = format( + "/bin/sh -c '" + "%s init --muhome=%s --maildir=%s --quiet; " + "%s index --muhome=%s --quiet'", + MU_PROGRAM, + test_dir.c_str(), + MU_TESTMAILDIR2, + MU_PROGRAM, + test_dir.c_str()); + + if (g_test_verbose()) + g_print("%s\n", cmdline.c_str()); + + GError *err{}; + if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, &err)) { + g_printerr("Error: %s\n", err ? err->message : "?"); + g_clear_error(&err); + g_assert(0); + } + + return test_dir; +} + +static void +test_something(const char* what) +{ + g_setenv("GUILE_AUTO_COMPILE", "0", TRUE); + g_setenv("GUILE_LOAD_PATH", GUILE_LOAD_PATH, TRUE); + g_setenv("GUILE_EXTENSIONS_PATH",GUILE_EXTENSIONS_PATH, TRUE); + + if (g_test_verbose()) + g_print("GUILE_LOAD_PATH: %s\n", GUILE_LOAD_PATH); + + const auto dir = fill_database(); + const auto cmdline = format("%s -q -e main %s/test-mu-guile.scm " + "--muhome=%s --test=%s", + GUILE_BINARY, + ABS_SRCDIR, + dir.c_str(), what); + + if (g_test_verbose()) + g_print("cmdline: %s\n", cmdline.c_str()); + + GError *err{}; + int status{}; + if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, &status, &err) || + status != 0) { + g_printerr("Error: %s\n", err ? err->message : "something went wrong"); + g_clear_error(&err); + g_assert(0); + } +} + +static void +test_mu_guile_queries(void) +{ + test_something("queries"); +} + +static void +test_mu_guile_messages(void) +{ + test_something("message"); +} + +static void +test_mu_guile_stats(void) +{ + test_something("stats"); +} + +int +main(int argc, char* argv[]) +{ + int rv; + TempDir tempdir; + test_dir = tempdir.path(); + + mu_test_init(&argc, &argv); + + if (!set_en_us_utf8_locale()) + return 0; /* don't error out... */ + + g_test_add_func("/guile/queries", test_mu_guile_queries); + g_test_add_func("/guile/message", test_mu_guile_messages); + g_test_add_func("/guile/stats", test_mu_guile_stats); + + rv = g_test_run(); + + return rv; +} diff --git a/guile/tests/test-mu-guile.scm b/guile/tests/test-mu-guile.scm new file mode 100755 index 0000000..d4d3740 --- /dev/null +++ b/guile/tests/test-mu-guile.scm @@ -0,0 +1,124 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +(setlocale LC_ALL "") + +(use-modules (srfi srfi-1)) +(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format)) +(use-modules (mu) (mu stats)) + +(define (n-results-or-exit query n) + "Run QUERY, and exit 1 if the number of results != N." + (let ((lst (mu:message-list query))) + (if (not (= (length lst) n)) + (begin + (simple-format (current-error-port) "Query: \"~A\"; expected ~A, got ~A\n" + query n (length lst)) + (exit 1))))) + +(define (test-queries) + "Test a bunch of queries (or die trying)." + (n-results-or-exit "hello" 1) + (n-results-or-exit "f:john fruit" 1) + (n-results-or-exit "f:soc@example.com" 1) + (n-results-or-exit "t:alki@example.com" 1) + (n-results-or-exit "t:alcibiades" 1) + (n-results-or-exit "f:soc@example.com OR f:john" 2) + (n-results-or-exit "f:soc@example.com OR f:john OR t:edmond" 3) + (n-results-or-exit "t:julius" 1) + (n-results-or-exit "s:dude" 1) + (n-results-or-exit "t:dantès" 1) + (n-results-or-exit "file:sittingbull.jpg" 1) + (n-results-or-exit "file:custer.jpg" 1) + (n-results-or-exit "file:custer.*" 1) + (n-results-or-exit "j:sit*" 1) + (n-results-or-exit "mime:image/jpeg" 1) + (n-results-or-exit "mime:text/plain" 13) + (n-results-or-exit "y:text*" 13) + (n-results-or-exit "y:image*" 1) + (n-results-or-exit "mime:message/rfc822" 2)) + +(define (error-exit msg . args) + "Print error and exit." + (let ((msg (apply format #f msg args))) + (simple-format (current-error-port) "*ERROR*: ~A\n" msg) + (exit 1))) + +(define (str-equal-or-exit got exp) + "S1 == S2 or exit 1." + ;; (format #t "'~A' <=> '~A'\n" s1 s2) + (if (not (string= exp got)) + (error-exit "Expected \"~A\", got \"~A\"\n" exp got))) + +(define (test-message) + "Test functions for a particular message." + + (let ((msg (car (mu:message-list "hello")))) + (str-equal-or-exit (mu:subject msg) "Fwd: rfc822") + (str-equal-or-exit (mu:to msg) "martin") + (str-equal-or-exit (mu:from msg) "foobar <foo@example.com>") + (str-equal-or-exit (mu:header msg "X-Mailer") "Ximian Evolution 1.4.5") + + (if (not (equal? (mu:priority msg) mu:prio:normal)) + (error-exit "Expected ~A, got ~A" (mu:priority msg) mu:prio:normal))) + + (let ((msg (car (mu:message-list "atoms")))) + (str-equal-or-exit (mu:subject msg) "atoms") + (str-equal-or-exit (mu:to msg) "Democritus <demo@example.com>") + (str-equal-or-exit (mu:from msg) "Richard P. Feynman <rpf@example.com>") + ;;(str-equal-or-exit (mu:header msg "Content-transfer-encoding") "7BIT") + + (if (not (equal? (mu:priority msg) mu:prio:high)) + (error-exit "Expected ~a, got ~a" (mu:priority msg) mu:prio:high)))) + +(define (num-equal-or-exit got exp) + "S1 == S2 or exit 1." + ;; (format #t "'~A' <=> '~A'\n" s1 s2) + (if (not (= exp got)) + (error-exit "Expected \"~S\", got \"~S\"\n" exp got))) + +(define (test-stats) + "Test statistical functions." + ;; average + (num-equal-or-exit (mu:average mu:size) 82152/13) + (num-equal-or-exit (floor (mu:stddev mu:size)) 13020.0) + (num-equal-or-exit (mu:max mu:size) 46308) + (num-equal-or-exit (mu:min mu:size) 111)) + +(define (main args) + (let* ((optionspec '((muhome (value #t)) + (test (value #t)))) + (options (getopt-long args optionspec)) + (muhome (option-ref options 'muhome #f)) + (test (option-ref options 'test #f))) + + (mu:initialize muhome) + + (if test + (cond + ((string= test "queries") (test-queries)) + ((string= test "message") (test-message)) + ((string= test "stats") (test-stats)) + (#t (exit 1)))))) + + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..f0fef68 --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,119 @@ +## Copyright (C) 2010-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# enforce compiling guile (optionally) first,then this dir first +# before descending into tests/ +include $(top_srcdir)/gtest.mk + +SUBDIRS= thirdparty utils message index + +TESTDEFS= \ + -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\" \ + -DMU_TESTMAILDIR2=\"${abs_srcdir}/testdir2\" \ + -DMU_TESTMAILDIR4=\"${abs_srcdir}/testdir4\" \ + -DABS_CURDIR=\"${abs_builddir}\" \ + -DABS_SRCDIR=\"${abs_srcdir}\" + + +AM_CFLAGS= \ + $(WARN_CFLAGS) \ + $(GMIME_CFLAGS) \ + $(XAPIAN_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(GUILE_CFLAGS) \ + $(ASAN_CFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + $(TESTDEFS) \ + -Wno-format-nonliteral \ + -Wno-switch-enum \ + -Wno-deprecated-declarations \ + -Wno-inline + +AM_CXXFLAGS= \ + $(GMIME_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(GUILE_CFLAGS) \ + $(WARN_CXXFLAGS) \ + $(XAPIAN_CXXFLAGS) \ + $(ASAN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + $(TESTDEFS) + +AM_CPPFLAGS= \ + $(CODE_COVERAGE_CPPFLAGS) + +noinst_LTLIBRARIES= \ + libmu.la + +libmu_la_SOURCES= \ + mu-bookmarks.cc \ + mu-bookmarks.hh \ + mu-contacts-cache.cc \ + mu-contacts-cache.hh \ + mu-parser.cc \ + mu-parser.hh \ + mu-query.cc \ + mu-query.hh \ + mu-query-results.hh \ + mu-query-match-deciders.cc \ + mu-query-match-deciders.hh \ + mu-query-threads.cc \ + mu-query-threads.hh \ + mu-runtime.cc \ + mu-runtime.hh \ + mu-script.cc \ + mu-script.hh \ + mu-server.cc \ + mu-server.hh \ + mu-store.cc \ + mu-store.hh \ + mu-tokenizer.cc \ + mu-tokenizer.hh \ + mu-tree.hh \ + mu-xapian.cc \ + mu-xapian.hh \ + mu-maildir.cc \ + mu-maildir.hh + +libmu_la_LIBADD= \ + $(XAPIAN_LIBS) \ + $(GMIME_LIBS) \ + $(GLIB_LIBS) \ + $(GUILE_LIBS) \ + ${builddir}/message/libmu-message.la \ + ${builddir}/index/libmu-index.la \ + $(CODE_COVERAGE_LIBS) + +libmu_la_LDFLAGS= \ + $(ASAN_LDFLAGS) + +noinst_PROGRAMS= \ + tokenize + +tokenize_SOURCES= \ + tokenize.cc + +tokenize_LDADD= \ + $(WARN_LDFLAGS) \ + libmu.la \ + utils/libmu-utils.la + +EXTRA_DIST= \ + doxyfile.in + +CLEANFILES=*.log *.trs *core* *vgdump* *.gcda *.gcno + +include $(top_srcdir)/aminclude_static.am diff --git a/lib/doxyfile.in b/lib/doxyfile.in new file mode 100644 index 0000000..6ad94ac --- /dev/null +++ b/lib/doxyfile.in @@ -0,0 +1,181 @@ +# Doxyfile 0.1 + +#--------------------------------------------------------------------------- +# General configuration options +#--------------------------------------------------------------------------- +PROJECT_NAME = mu +PROJECT_NUMBER = @VERSION@ +OUTPUT_DIRECTORY = apidocs +OUTPUT_LANGUAGE = English +EXTRACT_ALL = NO +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = NO +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ALWAYS_DETAILED_SEC = NO +FULL_PATH_NAMES = NO +STRIP_FROM_PATH = +INTERNAL_DOCS = NO +STRIP_CODE_COMMENTS = YES +CASE_SENSE_NAMES = YES +SHORT_NAMES = NO +HIDE_SCOPE_NAMES = NO +VERBATIM_HEADERS = YES +SHOW_INCLUDE_FILES = YES +JAVADOC_AUTOBRIEF = YES +INHERIT_DOCS = YES +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +DISTRIBUTE_GROUP_DOC = NO +TAB_SIZE = 8 +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +ALIASES = +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +OPTIMIZE_OUTPUT_FOR_C = YES +SHOW_USED_FILES = YES +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_FORMAT = +WARN_LOGFILE = +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = @top_srcdir@/lib/ +FILE_PATTERNS = *.c *.h +RECURSIVE = YES +EXCLUDE = tests + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. + +EXCLUDE_PATTERNS = Makefile.* ChangeLog CHANGES CHANGES.* README \ + README.* *.png AUTHORS DESIGN DESIGN.* *.desktop \ + DESKTOP* COMMENTS HOWTO magic NOTES TODO THANKS + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = +EXAMPLE_PATTERNS = +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_SOURCE_FILES = NO +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +REFERENCED_BY_RELATION = YES +REFERENCES_RELATION = YES +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- +ALPHABETICAL_INDEX = NO +COLS_IN_ALPHA_INDEX = 5 +IGNORE_PREFIX = +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- +GENERATE_HTML = YES +HTML_OUTPUT = +HTML_HEADER = +HTML_FOOTER = +HTML_STYLESHEET = +HTML_ALIGN_MEMBERS = YES +GENERATE_HTMLHELP = NO +GENERATE_CHI = NO +BINARY_TOC = NO +TOC_EXPAND = NO +DISABLE_INDEX = NO +ENUM_VALUES_PER_LINE = 4 +GENERATE_TREEVIEW = NO +TREEVIEW_WIDTH = 250 +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- +GENERATE_LATEX = NO +LATEX_OUTPUT = +COMPACT_LATEX = NO +PAPER_TYPE = a4wide +EXTRA_PACKAGES = +LATEX_HEADER = +PDF_HYPERLINKS = NO +USE_PDFLATEX = NO +LATEX_BATCHMODE = NO +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- +GENERATE_RTF = NO +RTF_OUTPUT = +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- +GENERATE_MAN = YES +MAN_OUTPUT = man +MAN_EXTENSION = .3mu +MAN_LINKS = YES +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- +GENERATE_XML = YES +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = "G_BEGIN_DECLS=" \ + "G_END_DECLS=" +# "DOXYGEN_SHOULD_SKIP_THIS" \ +# "DBUS_GNUC_DEPRECATED=" \ +# "_DBUS_DEFINE_GLOBAL_LOCK(name)=" \ +# "_DBUS_GNUC_PRINTF(from,to)=" +SKIP_FUNCTION_MACROS = YES +#--------------------------------------------------------------------------- +# Configuration::addtions related to external references +#--------------------------------------------------------------------------- +TAGFILES = +GENERATE_TAGFILE = +ALLEXTERNALS = NO +PERL_PATH = +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- +CLASS_DIAGRAMS = YES +HAVE_DOT = NO +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +TEMPLATE_RELATIONS = YES +HIDE_UNDOC_RELATIONS = YES +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +GRAPHICAL_HIERARCHY = YES +DOT_PATH = +DOTFILE_DIRS = +MAX_DOT_GRAPH_WIDTH = 640 +MAX_DOT_GRAPH_HEIGHT = 1024 +GENERATE_LEGEND = YES +DOT_CLEANUP = YES +#--------------------------------------------------------------------------- +# Configuration::addtions related to the search engine +#--------------------------------------------------------------------------- +SEARCHENGINE = NO diff --git a/lib/index/Makefile.am b/lib/index/Makefile.am new file mode 100644 index 0000000..25f62b2 --- /dev/null +++ b/lib/index/Makefile.am @@ -0,0 +1,46 @@ +## Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CPPFLAGS= \ + $(CODE_COVERAGE_CPPFLAGS) + +AM_CXXFLAGS= \ + $(WARN_CXXFLAGS) \ + $(GLIB_CFLAGS) \ + $(XAPIAN_CFLAGS) \ + $(ASAN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -I${top_srcdir}/lib + +AM_LDFLAGS= \ + $(ASAN_LDFLAGS) + +noinst_LTLIBRARIES= \ + libmu-index.la + +libmu_index_la_SOURCES= \ + mu-indexer.cc \ + mu-indexer.hh \ + mu-scanner.cc \ + mu-scanner.hh + +libmu_index_la_LIBADD= \ + $(GLIB_LIBS) \ + $(CODE_COVERAGE_LIBS) + +include $(top_srcdir)/aminclude_static.am diff --git a/lib/index/meson.build b/lib/index/meson.build new file mode 100644 index 0000000..34161ab --- /dev/null +++ b/lib/index/meson.build @@ -0,0 +1,37 @@ +## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +index_srcs=[ + 'mu-indexer.hh', + 'mu-indexer.cc', + 'mu-scanner.hh', + 'mu-scanner.cc' +] + +xapian_incs = xapian_dep.get_pkgconfig_variable('includedir') +lib_mu_index_inc_dep = declare_dependency( + include_directories: include_directories(['.', '..', xapian_incs])) +lib_mu_index=static_library('mu-index', [index_srcs], + dependencies: [ + config_h_dep, + glib_dep, + lib_mu_index_inc_dep + ], + install: false) + +lib_mu_index_dep = declare_dependency( + link_with: lib_mu_index +) diff --git a/lib/index/mu-indexer.cc b/lib/index/mu-indexer.cc new file mode 100644 index 0000000..8d96154 --- /dev/null +++ b/lib/index/mu-indexer.cc @@ -0,0 +1,456 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-indexer.hh" + +#include <config.h> + +#include <atomic> +#include <algorithm> +#include <mutex> +#include <vector> +#include <thread> +#include <condition_variable> +#include <iostream> +#include <atomic> +#include <chrono> +using namespace std::chrono_literals; + +#include "mu-scanner.hh" +#include "utils/mu-async-queue.hh" +#include "utils/mu-error.hh" +#include "../mu-store.hh" + +using namespace Mu; + +struct IndexState { + enum State { Idle, + Scanning, + Finishing, + Cleaning + }; + static const char* name(State s) { + switch (s) { + case Idle: + return "idle"; + case Scanning: + return "scanning"; + case Finishing: + return "finishing"; + case Cleaning: + return "cleaning"; + default: + return "<error>"; + } + } + + bool operator==(State rhs) const { + return state_.load() == rhs; + } + bool operator!=(State rhs) const { + return state_.load() != rhs; + } + void change_to(State new_state) { + g_debug("changing indexer state %s->%s", name((State)state_), + name((State)new_state)); + state_.store(new_state); + } + +private: + std::atomic<State> state_{Idle}; +}; + +struct Indexer::Private { + Private(Mu::Store& store) + : store_{store}, scanner_{store_.properties().root_maildir, + [this](auto&& path, auto&& statbuf, auto&& info) { + return handler(path, statbuf, info); + }}, + max_message_size_{store_.properties().max_message_size} { + g_message("created indexer for %s -> %s (batch-size: %zu)", + store.properties().root_maildir.c_str(), + store.properties().database_path.c_str(), store.properties().batch_size); + } + + ~Private() { + stop(); + } + + bool dir_predicate(const std::string& path, const struct dirent* dirent) const; + bool handler(const std::string& fullpath, struct stat* statbuf, Scanner::HandleType htype); + + void maybe_start_worker(); + void item_worker(); + void scan_worker(); + + bool add_message(const std::string& path); + + bool cleanup(); + bool start(const Indexer::Config& conf); + bool stop(); + + Indexer::Config conf_; + Store& store_; + Scanner scanner_; + const size_t max_message_size_; + + time_t dirstamp_{}; + std::size_t max_workers_; + std::vector<std::thread> workers_; + std::thread scanner_worker_; + + struct WorkItem { + std::string full_path; + enum Type { + Dir, + File + }; + Type type; + }; + + AsyncQueue<WorkItem> todos_; + + Progress progress_; + IndexState state_; + std::mutex lock_, w_lock_; + + std::atomic<time_t> completed_; +}; + +bool +Indexer::Private::handler(const std::string& fullpath, struct stat* statbuf, + Scanner::HandleType htype) +{ + switch (htype) { + case Scanner::HandleType::EnterDir: + case Scanner::HandleType::EnterNewCur: { + // in lazy-mode, we ignore this dir if its dirstamp suggest it + // is up-to-date (this is _not_ always true; hence we call it + // lazy-mode); only for actual message dirs, since the dir + // tstamps may not bubble up. + dirstamp_ = store_.dirstamp(fullpath); + if (conf_.lazy_check && dirstamp_ >= statbuf->st_ctime && + htype == Scanner::HandleType::EnterNewCur) { + g_debug("skip %s (seems up-to-date: %s >= %s)", fullpath.c_str(), + time_to_string("%FT%T", dirstamp_).c_str(), + time_to_string("%FT%T", statbuf->st_ctime).c_str()); + return false; + } + + // don't index dirs with '.noindex' + auto noindex = ::access((fullpath + "/.noindex").c_str(), F_OK) == 0; + if (noindex) { + g_debug("skip %s (has .noindex)", fullpath.c_str()); + return false; // don't descend into this dir. + } + + // don't index dirs with '.noupdate', unless we do a full + // (re)index. + if (!conf_.ignore_noupdate) { + auto noupdate = ::access((fullpath + "/.noupdate").c_str(), F_OK) == 0; + if (noupdate) { + g_debug("skip %s (has .noupdate)", fullpath.c_str()); + return false; + } + } + + g_debug("checked %s", fullpath.c_str()); + return true; + } + case Scanner::HandleType::LeaveDir: { + todos_.push({fullpath, WorkItem::Type::Dir}); + return true; + } + + case Scanner::HandleType::File: { + ++progress_.checked; + + if ((size_t)statbuf->st_size > max_message_size_) { + g_debug("skip %s (too big: %" G_GINT64_FORMAT " bytes)", fullpath.c_str(), + (gint64)statbuf->st_size); + return false; + } + + // if the message is not in the db yet, or not up-to-date, queue + // it for updating/inserting. + if (statbuf->st_ctime <= dirstamp_ && store_.contains_message(fullpath)) { + // g_debug ("skip %s: already up-to-date"); + return false; + } + + // push the remaining messages to our "todo" queue for + // (re)parsing and adding/updating to the database. + todos_.push({fullpath, WorkItem::Type::File}); + return true; + } + default: + g_return_val_if_reached(false); + return false; + } +} + +void +Indexer::Private::maybe_start_worker() +{ + std::lock_guard lock{w_lock_}; + + if (todos_.size() > workers_.size() && workers_.size() < max_workers_) { + workers_.emplace_back(std::thread([this] { item_worker(); })); + g_debug("added worker %zu", workers_.size()); + } +} + +bool +Indexer::Private::add_message(const std::string& path) +{ + /* + * Having the lock here makes things a _lot_ slower. + * + * The reason for having the lock is some helgrind warnings; + * but it believed those are _false alarms_ + * https://gitlab.gnome.org/GNOME/glib/-/issues/2662 + * + * std::unique_lock lock{w_lock_}; + */ + auto msg{Message::make_from_path(path)}; + if (!msg) { + g_warning("failed to create message from %s: %s", + path.c_str(), msg.error().what()); + return false; + } + auto res = store_.add_message(msg.value(), true /*use-transaction*/); + if (!res) { + g_warning("failed to add message @ %s: %s", + path.c_str(), res.error().what()); + return false; + } + + return true; +} + +void +Indexer::Private::item_worker() +{ + WorkItem item; + + g_debug("started worker"); + + while (state_ == IndexState::Scanning) { + if (!todos_.pop(item, 250ms)) + continue; + try { + switch (item.type) { + case WorkItem::Type::File: { + if (G_LIKELY(add_message(item.full_path))) + ++progress_.updated; + } break; + case WorkItem::Type::Dir: + store_.set_dirstamp(item.full_path, ::time(NULL)); + break; + default: + g_warn_if_reached(); + break; + } + } catch (const Mu::Error& er) { + g_warning("error adding message @ %s: %s", + item.full_path.c_str(), er.what()); + } + + maybe_start_worker(); + std::this_thread::yield(); + } +} + +bool +Indexer::Private::cleanup() +{ + g_debug("starting cleanup"); + + size_t n{}; + std::vector<Store::Id> orphans; // store messages without files. + store_.for_each_message_path([&](Store::Id id, const std::string& path) { + ++n; + if (::access(path.c_str(), R_OK) != 0) { + g_debug("cannot read %s (id=%u); queueing for removal from store", + path.c_str(), id); + orphans.emplace_back(id); + } + + return state_ == IndexState::Cleaning; + }); + + if (orphans.empty()) + g_debug("nothing to clean up"); + else { + g_debug("removing up %zu stale message(s) from store", orphans.size()); + store_.remove_messages(orphans); + progress_.removed += orphans.size(); + } + + return true; +} + +void +Indexer::Private::scan_worker() +{ + progress_.reset(); + + if (conf_.scan) { + g_debug("starting scanner"); + if (!scanner_.start()) { // blocks. + g_warning("failed to start scanner"); + state_.change_to(IndexState::Idle); + return; + } + g_debug("scanner finished with %zu file(s) in queue", todos_.size()); + } + + // now there may still be messages in the work queue... + // finish those; this is a bit ugly; perhaps we should + // handle SIGTERM etc. + + if (!todos_.empty()) { + const auto workers_size = std::invoke([this] { + std::lock_guard lock{w_lock_}; + return workers_.size(); + }); + g_debug("process %zu remaining message(s) with %zu worker(s)", + todos_.size(), workers_size); + while (!todos_.empty()) + std::this_thread::sleep_for(100ms); + } + // and let the worker finish their work. + state_.change_to(IndexState::Finishing); + for (auto&& w : workers_) + if (w.joinable()) + w.join(); + + if (conf_.cleanup) { + g_debug("starting cleanup"); + + state_.change_to(IndexState::Cleaning); + cleanup(); + g_debug("cleanup finished"); + } + + completed_ = ::time({}); + state_.change_to(IndexState::Idle); +} + +bool +Indexer::Private::start(const Indexer::Config& conf) +{ + stop(); + + conf_ = conf; + if (conf_.max_threads == 0) { + /* benchmarking suggests that ~4 threads is the fastest (the + * real bottleneck is the database, so adding more threads just + * slows things down) + */ + max_workers_ = std::min(4U, std::thread::hardware_concurrency()); + } else + max_workers_ = conf.max_threads; + + g_debug("starting indexer with <= %zu worker thread(s)", max_workers_); + g_debug("indexing: %s; clean-up: %s", conf_.scan ? "yes" : "no", + conf_.cleanup ? "yes" : "no"); + + state_.change_to(IndexState::Scanning); + /* kick off the first worker, which will spawn more if needed. */ + workers_.emplace_back(std::thread([this] { item_worker(); })); + /* kick the disk-scanner thread */ + scanner_worker_ = std::thread([this] { scan_worker(); }); + + g_debug("started indexer"); + + return true; +} + +bool +Indexer::Private::stop() +{ + scanner_.stop(); + + todos_.clear(); + if (scanner_worker_.joinable()) + scanner_worker_.join(); + + state_.change_to(IndexState::Idle); + for (auto&& w : workers_) + if (w.joinable()) + w.join(); + workers_.clear(); + + return true; +} + +Indexer::Indexer(Store& store) + : priv_{std::make_unique<Private>(store)} +{} + +Indexer::~Indexer() = default; + +bool +Indexer::start(const Indexer::Config& conf) +{ + const auto mdir{priv_->store_.properties().root_maildir}; + if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0)) { + g_critical("'%s' is not readable: %s", mdir.c_str(), g_strerror(errno)); + return false; + } + + std::lock_guard lock(priv_->lock_); + if (is_running()) + return true; + + return priv_->start(conf); +} + +bool +Indexer::stop() +{ + std::lock_guard lock{priv_->lock_}; + + if (!is_running()) + return true; + + g_debug("stopping indexer"); + return priv_->stop(); +} + +bool +Indexer::is_running() const +{ + return priv_->state_ != IndexState::Idle; +} + +const Indexer::Progress& +Indexer::progress() const +{ + priv_->progress_.running = priv_->state_ == IndexState::Idle ? false : true; + + return priv_->progress_; +} + +time_t +Indexer::completed() const +{ + return priv_->completed_; +} diff --git a/lib/index/mu-indexer.hh b/lib/index/mu-indexer.hh new file mode 100644 index 0000000..0c678a6 --- /dev/null +++ b/lib/index/mu-indexer.hh @@ -0,0 +1,126 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_INDEXER_HH__ +#define MU_INDEXER_HH__ + +#include <atomic> +#include <memory> +#include <chrono> + +namespace Mu { + +class Store; + +/// An object abstracting the index process. +class Indexer { +public: + /** + * Construct an indexer object + * + * @param store the message store to use + */ + Indexer(Store& store); + + /** + * DTOR + */ + ~Indexer(); + + /// A configuration object for the indexer + struct Config { + bool scan{true}; + /**< scan for new messages */ + bool cleanup{true}; + /**< clean messages no longer in the file system */ + size_t max_threads{}; + /**< maximum # of threads to use */ + bool ignore_noupdate{}; + /**< ignore .noupdate files */ + bool lazy_check{}; + /**< whether to skip directories that don't have a changed + * mtime */ + }; + + /** + * Start indexing. If already underway, do nothing. This returns + * immediately after starting, with the work being done in the + * background. + * + * @param conf a configuration object + * + * @return true if starting worked or an indexing process was already + * underway; false otherwise. + * + */ + bool start(const Config& conf); + + /** + * Stop indexing. If not indexing, do nothing. + * + * + * @return true if we stopped indexing, or indexing was not underway. + * False otherwise. + */ + bool stop(); + + /** + * Is an indexing process running? + * + * @return true or false. + */ + bool is_running() const; + + // Object describing current progress + struct Progress { + void reset() + { + running = false; + checked = updated = removed = 0; + } + + std::atomic<bool> running{}; /**< Is an index operation in progress? */ + std::atomic<size_t> checked{}; /**< Number of messages checked for changes */ + std::atomic<size_t> updated{}; /**< Number of messages (re)parsed/added/updated */ + std::atomic<size_t> removed{}; /**< Number of message removed from store */ + }; + + /** + * Get an object describing the current progress. The progress object + * describes the most recent indexing job, and is reset upon a fresh + * start(). + * + * @return a progress object. + */ + const Progress& progress() const; + + /** + * Last time indexing was completed. + * + * @return the time or 0 + */ + time_t completed() const; + +private: + struct Private; + std::unique_ptr<Private> priv_; +}; + +} // namespace Mu +#endif /* MU_INDEXER_HH__ */ diff --git a/lib/index/mu-scanner.cc b/lib/index/mu-scanner.cc new file mode 100644 index 0000000..dfa6984 --- /dev/null +++ b/lib/index/mu-scanner.cc @@ -0,0 +1,232 @@ +/* +** Copyright (C) 2020-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include "mu-scanner.hh" + +#include "config.h" + +#include <chrono> +#include <mutex> +#include <atomic> +#include <thread> +#include <cstring> + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <glib.h> + +#include "utils/mu-utils.hh" +#include "utils/mu-error.hh" + +using namespace Mu; + +struct Scanner::Private { + Private(const std::string& root_dir, Scanner::Handler handler) + : root_dir_{root_dir}, handler_{handler} + { + if (!handler_) + throw Mu::Error{Error::Code::Internal, "missing handler"}; + } + ~Private() + { + stop(); + } + + bool start(); + bool stop(); + bool process_dentry(const std::string& path, struct dirent* dentry, bool is_maildir); + bool process_dir(const std::string& path, bool is_maildir); + + const std::string root_dir_; + const Scanner::Handler handler_; + std::atomic<bool> running_{}; + std::mutex lock_; +}; + +static bool +is_special_dir(const char* d_name) +{ + return d_name[0] == '\0' || (d_name[1] == '\0' && d_name[0] == '.') || + (d_name[2] == '\0' && d_name[0] == '.' && d_name[1] == '.'); +} + +bool +Scanner::Private::process_dentry(const std::string& path, struct dirent* dentry, + bool is_maildir) +{ + const auto d_name{dentry->d_name}; + + if (is_special_dir(d_name) || std::strcmp(d_name, "tmp") == 0) + return true; // ignore. + + const auto fullpath{path + "/" + d_name}; + struct stat statbuf { + }; + if (::stat(fullpath.c_str(), &statbuf) != 0) { + g_warning("failed to stat %s: %s", fullpath.c_str(), g_strerror(errno)); + return false; + } + + if (S_ISDIR(statbuf.st_mode)) { + const auto new_cur = + std::strcmp(d_name, "cur") == 0 || std::strcmp(d_name, "new") == 0; + const auto htype = + new_cur ? Scanner::HandleType::EnterNewCur : Scanner::HandleType::EnterDir; + const auto res = handler_(fullpath, &statbuf, htype); + if (!res) + return true; // skip + + process_dir(fullpath, new_cur); + + return handler_(fullpath, &statbuf, Scanner::HandleType::LeaveDir); + + } else if (S_ISREG(statbuf.st_mode) && is_maildir) + return handler_(fullpath, &statbuf, Scanner::HandleType::File); + + g_debug("skip %s (neither maildir-file nor directory)", fullpath.c_str()); + + return true; +} + +bool +Scanner::Private::process_dir(const std::string& path, bool is_maildir) +{ + if (!running_) + return true; /* we're done */ + + const auto dir = opendir(path.c_str()); + if (G_UNLIKELY(!dir)) { + g_warning("failed to scan dir %s: %s", path.c_str(), g_strerror(errno)); + return false; + } + + // TODO: sort dentries by inode order, which makes things faster for extfs. + // see mu-maildir.c + + while (running_) { + errno = 0; + const auto dentry{readdir(dir)}; + + if (G_LIKELY(dentry)) { + process_dentry(path, dentry, is_maildir); + continue; + } + + if (errno != 0) { + g_warning("failed to read %s: %s", path.c_str(), g_strerror(errno)); + continue; + } + + break; + } + closedir(dir); + + return true; +} + +bool +Scanner::Private::start() +{ + const auto& path{root_dir_}; + if (G_UNLIKELY(path.length() > PATH_MAX)) { + g_warning("path too long"); + return false; + } + + const auto mode{F_OK | R_OK}; + if (G_UNLIKELY(access(path.c_str(), mode) != 0)) { + g_warning("'%s' is not readable: %s", path.c_str(), g_strerror(errno)); + return false; + } + + struct stat statbuf { + }; + if (G_UNLIKELY(stat(path.c_str(), &statbuf) != 0)) { + g_warning("'%s' is not stat'able: %s", path.c_str(), g_strerror(errno)); + return false; + } + + if (G_UNLIKELY(!S_ISDIR(statbuf.st_mode))) { + g_warning("'%s' is not a directory", path.c_str()); + return false; + } + + running_ = true; + g_debug("starting scan @ %s", root_dir_.c_str()); + + auto basename{g_path_get_basename(root_dir_.c_str())}; + const auto is_maildir = + (g_strcmp0(basename, "cur") == 0 || g_strcmp0(basename, "new") == 0); + g_free(basename); + + const auto start{std::chrono::steady_clock::now()}; + process_dir(root_dir_, is_maildir); + const auto elapsed = std::chrono::steady_clock::now() - start; + g_debug("finished scan of %s in %" G_GINT64_FORMAT " ms", root_dir_.c_str(), + to_ms(elapsed)); + running_ = false; + + return true; +} + +bool +Scanner::Private::stop() +{ + if (!running_) + return true; // nothing to do + + g_debug("stopping scan"); + running_ = false; + + return true; +} + +Scanner::Scanner(const std::string& root_dir, Scanner::Handler handler) + : priv_{std::make_unique<Private>(root_dir, handler)} +{ +} + +Scanner::~Scanner() = default; + +bool +Scanner::start() +{ + if (priv_->running_) + return true; // nothing to do + + const auto res = priv_->start(); /* blocks */ + priv_->running_ = false; + + return res; +} + +bool +Scanner::stop() +{ + std::lock_guard l(priv_->lock_); + + return priv_->stop(); +} + +bool +Scanner::is_running() const +{ + return priv_->running_; +} diff --git a/lib/index/mu-scanner.hh b/lib/index/mu-scanner.hh new file mode 100644 index 0000000..0d2f1c1 --- /dev/null +++ b/lib/index/mu-scanner.hh @@ -0,0 +1,100 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_SCANNER_HH__ +#define MU_SCANNER_HH__ + +#include <functional> +#include <memory> + +#include <dirent.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +namespace Mu { + +/// @brief Maildir scanner +/// +/// Scans maildir (trees) recursively, and calls the Handler callback for +/// directories & files. +/// +/// It filters out (i.e., does *not* call the handler for): +/// - files starting with '.' +/// - files that do not live in a cur / new leaf maildir +/// - directories '.' and '..' and 'tmp' +/// +class Scanner { + public: + enum struct HandleType { + File, + EnterNewCur, /* cur/ or new/ */ + EnterDir, /* some other directory */ + LeaveDir + }; + + /// Prototype for a handler function + using Handler = std::function< + bool(const std::string& fullpath, struct stat* statbuf, HandleType htype)>; + /** + * Construct a scanner object for scanning a directory, recursively. + * + * If handler is a directory + * + * + * @param root_dir root dir to start scanning + * @param handler handler function for some direntry + */ + Scanner(const std::string& root_dir, Handler handler); + + /** + * DTOR + */ + ~Scanner(); + + /** + * Start the scan; this is a blocking call than runs until + * finished or (from another thread) stop() is called. + * + * @return true if starting worked; false otherwise + */ + bool start(); + + /** + * Stop the scan + * + * @return true if stopping worked; false otherwi%sse + */ + bool stop(); + + /** + * Is a scan currently running? + * + * @return true or false + */ + bool is_running() const; + + private: + struct Private; + std::unique_ptr<Private> priv_; +}; + +} // namespace Mu + +#endif /* MU_SCANNER_HH__ */ diff --git a/lib/index/test-scanner.cc b/lib/index/test-scanner.cc new file mode 100644 index 0000000..4835aa1 --- /dev/null +++ b/lib/index/test-scanner.cc @@ -0,0 +1,65 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <vector> +#include <glib.h> + +#include <iostream> +#include <sstream> + +#include "mu-scanner.hh" +#include "mu-utils.hh" + +using namespace Mu; + +static void +test_scan_maildir() +{ + allow_warnings(); + + Scanner scanner{ + "/home/djcb/Maildir", + [](const dirent* dentry) -> bool { + g_print("%02x %s\n", dentry->d_type, dentry->d_name); + return true; + }, + [](const std::string& fullpath, const struct stat* statbuf, auto&& info) -> bool { + g_print("%s %zu\n", fullpath.c_str(), statbuf->st_size); + return true; + }}; + g_assert_true(scanner.start()); + + while (scanner.is_running()) { + sleep(1); + } +} + +int +main(int argc, char* argv[]) +try { + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/utils/scanner/scan-maildir", test_scan_maildir); + + return g_test_run(); + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} diff --git a/lib/meson.build b/lib/meson.build new file mode 100644 index 0000000..eb04859 --- /dev/null +++ b/lib/meson.build @@ -0,0 +1,81 @@ +## Copyright (C) 2021-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +subdir('utils') +subdir('message') +subdir('index') + +lib_mu=static_library( + 'mu', + [ + 'mu-bookmarks.cc', + 'mu-contacts-cache.cc', + 'mu-maildir.cc', + 'mu-parser.cc', + 'mu-query-match-deciders.cc', + 'mu-query-threads.cc', + 'mu-query.cc', + 'mu-runtime.cc', + 'mu-script.cc', + 'mu-server.cc', + 'mu-store.cc', + 'mu-tokenizer.cc', + 'mu-xapian.cc' + ], + dependencies: [ + glib_dep, + gio_dep, + gmime_dep, + xapian_dep, + guile_dep, + config_h_dep, + lib_mu_utils_dep, + lib_mu_message_dep, + lib_mu_index_dep + ], + install: false) + + +lib_mu_dep = declare_dependency( + link_with: lib_mu, + dependencies: [ lib_mu_message_dep, thread_dep ], + include_directories: + include_directories(['.', '..'])) + +# dev helpers +tokenize = executable( + 'tokenize', + [ 'mu-tokenizer.cc', 'tokenize.cc' ], + dependencies: [ lib_mu_utils_dep, glib_dep ], + install: false) + +# actual tests + +test('test-threads', + executable('test-threads', + 'mu-query-threads.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, lib_mu_dep])) +test('test-contacts-cache', + executable('test-contacts-cache', + 'mu-contacts-cache.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, lib_mu_dep])) + +subdir('tests') diff --git a/lib/message/Makefile.am b/lib/message/Makefile.am new file mode 100644 index 0000000..d10815f --- /dev/null +++ b/lib/message/Makefile.am @@ -0,0 +1,51 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +include $(top_srcdir)/gtest.mk + +AM_CXXFLAGS= \ + $(WARN_CXXFLAGS) \ + $(GLIB_CFLAGS) \ + $(GMIME_CFLAGS) \ + $(XAPIAN_CFLAGS) \ + -I${top_srcdir}/lib + +noinst_LTLIBRARIES= \ + libmu-message.la + +libmu_message_la_SOURCES= \ + mu-message.cc \ + mu-message.hh \ + mu-message-file.cc \ + mu-message-file.hh \ + mu-message-part.cc \ + mu-message-part.hh \ + mu-contact.hh \ + mu-contact.cc \ + mu-document.cc \ + mu-document.hh \ + mu-fields.hh \ + mu-fields.cc \ + mu-flags.hh \ + mu-flags.cc \ + mu-priority.hh \ + mu-priority.cc \ + mu-mime-object.cc \ + mu-mime-object.hh + +libmu_message_la_LIBADD= \ + $(GLIB_LIBS) \ + $(GMIME_LIBS) \ + $(XAPIAN_LIBS) diff --git a/lib/message/meson.build b/lib/message/meson.build new file mode 100644 index 0000000..3997fe2 --- /dev/null +++ b/lib/message/meson.build @@ -0,0 +1,95 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +lib_mu_message=static_library( + 'mu-message', + [ + 'mu-message.cc', + 'mu-message-file.cc', + 'mu-message-part.cc', + 'mu-contact.cc', + 'mu-document.cc', + 'mu-fields.cc', + 'mu-flags.cc', + 'mu-priority.cc', + 'mu-mime-object.cc', + ], + dependencies: [ + glib_dep, + gmime_dep, + xapian_dep, + config_h_dep, + lib_mu_utils_dep], + install: false) + +lib_mu_message_dep = declare_dependency( + link_with: lib_mu_message, + dependencies: [ xapian_dep, gmime_dep ], + include_directories: + include_directories(['.', '..'])) + +# +# tests +# + +test('test-contact', + executable('test-contact', + 'mu-contact.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-document', + executable('test-document', + 'mu-document.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-fields', + executable('test-fields', + 'mu-fields.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-flags', + executable('test-flags', + 'mu-flags.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-message', + executable('test-message', + 'test-mu-message.cc', + install: false, + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-priority', + executable('test-priority', + 'mu-priority.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-message-file', + executable('test-message-file', + 'mu-message-file.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, lib_mu_message_dep])) diff --git a/lib/message/mu-contact.cc b/lib/message/mu-contact.cc new file mode 100644 index 0000000..711f4d2 --- /dev/null +++ b/lib/message/mu-contact.cc @@ -0,0 +1,211 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-contact.hh" +#include "mu-message.hh" +#include "utils/mu-utils.hh" +#include "mu-mime-object.hh" + +#include <gmime/gmime.h> +#include <glib.h> +#include <string> + +using namespace Mu; + +static bool +needs_quoting(const std::string& name) +{ + for (auto& c: name) + if (c == ',' || c == '"') + return true; + return false; +} + +std::string +Contact::display_name(bool quote) const +{ + + if (name.empty()) + return email; + else if (!quote || !needs_quoting(name)) + return name + " <" + email + '>'; + else + return address_rfc2047(*this); +} + +std::string +Mu::to_string(const Mu::Contacts& contacts) +{ + std::string res; + + seq_for_each(contacts, [&](auto&& contact) { + if (res.empty()) + res = contact.display_name(); + else + res += ", " + contact.display_name(); + }); + + return res; +} + +size_t +Mu::lowercase_hash(const std::string& s) +{ + std::size_t djb = 5381; // djb hash + for (const auto c : s) + djb = ((djb << 5) + djb) + + static_cast<size_t>(g_ascii_tolower(c)); + return djb; +} + +#ifdef BUILD_TESTS +/* + * Tests. + * + */ + +#include "utils/mu-test-utils.hh" + +static void +test_ctor_foo() +{ + Contact c{ + "foo@example.com", + "Foo Bar", + Contact::Type::Bcc, + 1645214647 + }; + + assert_equal(c.email, "foo@example.com"); + assert_equal(c.name, "Foo Bar"); + g_assert_true(*c.field_id() == Field::Id::Bcc); + g_assert_cmpuint(c.message_date,==,1645214647); + + assert_equal(c.display_name(), "Foo Bar <foo@example.com>"); +} + + +static void +test_ctor_blinky() +{ + Contact c{ + "bar@example.com", + "Blinky", + 1645215014, + true, /* personal */ + 13, /*freq*/ + 12345 /* tstamp */ + }; + + assert_equal(c.email, "bar@example.com"); + assert_equal(c.name, "Blinky"); + g_assert_true(c.personal); + g_assert_cmpuint(c.frequency,==,13); + g_assert_cmpuint(c.tstamp,==,12345); + g_assert_cmpuint(c.message_date,==,1645215014); + + assert_equal(c.display_name(), "Blinky <bar@example.com>"); +} + +static void +test_ctor_cleanup() +{ + Contact c{ + "bar@example.com", + "Bli\nky", + 1645215014, + true, /* personal */ + 13, /*freq*/ + 12345 /* tstamp */ + }; + + assert_equal(c.email, "bar@example.com"); + assert_equal(c.name, "Bli ky"); + g_assert_true(c.personal); + g_assert_cmpuint(c.frequency,==,13); + g_assert_cmpuint(c.tstamp,==,12345); + g_assert_cmpuint(c.message_date,==,1645215014); + + assert_equal(c.display_name(), "Bli ky <bar@example.com>"); +} + +static void +test_encode() +{ + Contact c{ + "cassius@example.com", + "Ali, Muhammad \"The Greatest\"", + 345, + false, /* personal */ + 333, /*freq*/ + 768 /* tstamp */ + }; + + assert_equal(c.email, "cassius@example.com"); + assert_equal(c.name, "Ali, Muhammad \"The Greatest\""); + g_assert_false(c.personal); + g_assert_cmpuint(c.frequency,==,333); + g_assert_cmpuint(c.tstamp,==,768); + g_assert_cmpuint(c.message_date,==,345); + + assert_equal(c.display_name(true), + "\"Ali, Muhammad \\\"The Greatest\\\"\" <cassius@example.com>"); +} + + +static void +test_sender() +{ + Contact c{"aa@example.com", "Anders Ångström", + Contact::Type::Sender, 54321}; + + assert_equal(c.email, "aa@example.com"); + assert_equal(c.name, "Anders Ångström"); + g_assert_false(c.personal); + g_assert_cmpuint(c.frequency,==,1); + g_assert_cmpuint(c.message_date,==,54321); + + g_assert_false(!!c.field_id()); +} + + +static void +test_misc() +{ + g_assert_false(!!contact_type_from_field_id(Field::Id::Subject)); +} + + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + g_mime_init(); + + g_test_add_func("/message/contact/ctor-foo", test_ctor_foo); + g_test_add_func("/message/contact/ctor-blinky", test_ctor_blinky); + g_test_add_func("/message/contact/ctor-cleanup", test_ctor_cleanup); + g_test_add_func("/message/contact/encode", test_encode); + + g_test_add_func("/message/contact/sender", test_sender); + g_test_add_func("/message/contact/misc", test_misc); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-contact.hh b/lib/message/mu-contact.hh new file mode 100644 index 0000000..06d9d16 --- /dev/null +++ b/lib/message/mu-contact.hh @@ -0,0 +1,212 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_MESSAGE_CONTACT_HH__ +#define MU_MESSAGE_CONTACT_HH__ + +#include <functional> +#include <string> +#include <vector> +#include <functional> +#include <cctype> +#include <cstring> +#include <cstdlib> +#include <ctime> + +#include <utils/mu-option.hh> +#include "mu-fields.hh" + +struct _InternetAddressList; + +namespace Mu { + +/** + * Get the hash value for a lowercase value of s; useful for email-addresses + * + * @param s a string + * + * @return a hash value. + */ +size_t lowercase_hash(const std::string& s); + +struct Contact { + enum struct Type { + None, Sender, From, ReplyTo, To, Cc, Bcc + }; + + /** + * Construct a new Contact + * + * @param email_ email address + * @param name_ name or empty + * @param type_ contact field type + * @param message_date_ data for the message for this contact + */ + Contact(const std::string& email_, const std::string& name_ = "", + Type type_ = Type::None, ::time_t message_date_ = 0) + : email{email_}, name{name_}, type{type_}, + message_date{message_date_}, personal{}, frequency{1}, tstamp{} + { cleanup_name(); } + + /** + * Construct a new Contact + * + * @param email_ email address + * @param name_ name or empty + * @param message_date_ date of message this contact originate from + * @param personal_ is this a personal contact? + * @param freq_ how often was this contact seen? + * @param tstamp_ timestamp for last change + */ + Contact(const std::string& email_, const std::string& name_, + time_t message_date_, bool personal_, size_t freq_, + int64_t tstamp_) + : email{email_}, name{name_}, type{Type::None}, + message_date{message_date_}, personal{personal_}, frequency{freq_}, + tstamp{tstamp_} + { cleanup_name();} + + /** + * Get the "display name" for this contact; basically, if there's a + * non-empty name, it's + * Jane Doe <email@example.com> + * otherwise it's just the e-mail address. + * + * @param quote_if_needed if true, handle quoting of the name-part as well. This + * is useful when the address is to be used directly in emails. + * + * @return the display name + */ + std::string display_name(bool quote_if_needed=false) const; + + + /** + * Operator==; based on the hash values (ie. lowercase e-mail address) + * + * @param rhs some other Contact + * + * @return true orf false. + */ + bool operator== (const Contact& rhs) const noexcept { + return hash() == rhs.hash(); + } + + /** + * Get a hash-value for this contact, which gets lazily calculated. This + * * is for use with container classes. This uses the _lowercase_ email + * address. + * + * @return the hash + */ + size_t hash() const { + static size_t cached_hash; + if (cached_hash == 0) { + cached_hash = lowercase_hash(email); + } + return cached_hash; + } + + /** + * Get the corresponding Field::Id (if any) + * for this contact. + * + * @return the field-id or Nothing. + */ + constexpr Option<Field::Id> field_id() const noexcept { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch(type) { + case Type::Bcc: + return Field::Id::Bcc; + case Type::Cc: + return Field::Id::Cc; + case Type::From: + return Field::Id::From; + case Type::To: + return Field::Id::To; + default: + return Nothing; + } +#pragma GCC diagnostic pop + } + + + /* + * data members + */ + + std::string email; /**< Email address for this contact.Not empty */ + std::string name; /**< Name for this contact; can be empty. */ + Type type; /**< Type of contact */ + int64_t message_date; /**< date of the contact's message */ + bool personal; /**< A personal message? */ + size_t frequency; /**< Frequency of this contact */ + int64_t tstamp; /**< Timestamp for this contact (internal use) */ + +private: + void cleanup_name() { // replace control characters by spaces. + for (auto& c: name) + if (iscntrl(c)) + c = ' '; + } +}; + +constexpr Option<Contact::Type> +contact_type_from_field_id(Field::Id id) noexcept { + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch(id) { + case Field::Id::Bcc: + return Contact::Type::Bcc; + case Field::Id::Cc: + return Contact::Type::Cc; + case Field::Id::From: + return Contact::Type::From; + case Field::Id::To: + return Contact::Type::To; + default: + return Nothing; + } +#pragma GCC diagnostic pop +} + +using Contacts = std::vector<Contact>; + +/** + * Get contacts as a comma-separated list. + * + * @param contacts contacs + * + * @return string with contacts. + */ +std::string to_string(const Contacts& contacts); + +} // namespace Mu + +/** + * Implement our hash int std:: + */ +template<> struct std::hash<Mu::Contact> { + std::size_t operator()(const Mu::Contact& c) const noexcept { + return c.hash(); + } +}; + +#endif /* MU_CONTACT_HH__ */ diff --git a/lib/message/mu-document.cc b/lib/message/mu-document.cc new file mode 100644 index 0000000..f0c230c --- /dev/null +++ b/lib/message/mu-document.cc @@ -0,0 +1,513 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-document.hh" +#include "mu-message.hh" + +#include <cstdint> +#include <glib.h> +#include <numeric> +#include <algorithm> +#include <charconv> +#include <cinttypes> + +#include <string> +#include <utils/mu-utils.hh> + +using namespace Mu; + +constexpr uint8_t SepaChar1 = 0xfe; +constexpr uint8_t SepaChar2 = 0xff; + +static void +add_search_term(Xapian::Document& doc, const Field& field, const std::string& val) +{ + if (field.is_normal_term()) { + doc.add_term(field.xapian_term(val)); + } else if (field.is_boolean_term()) { + doc.add_boolean_term(field.xapian_term(val)); + } else if (field.is_indexable_term()) { + Xapian::TermGenerator termgen; + termgen.set_document(doc); + termgen.index_text(utf8_flatten(val), 1, field.xapian_term()); + /* also add as 'normal' term, so some queries where the indexer + * eats special chars also match */ + if (field.id != Field::Id::BodyText && + field.id != Field::Id::EmbeddedText) { + doc.add_term(field.xapian_term(val)); + } + } else + throw std::logic_error("not a search term"); +} + + +static std::string +make_prop_name(const Field& field) +{ + return ":" + std::string(field.name); +} + +void +Document::add(Field::Id id, const std::string& val) +{ + const auto field{field_from_id(id)}; + + if (field.is_value()) + xdoc_.add_value(field.value_no(), val); + + if (field.is_searchable()) + add_search_term(xdoc_, field, val); + + if (field.include_in_sexp()) + sexp_list().add_prop(make_prop_name(field), + Sexp::make_string(std::move(val))); +} + +void +Document::add(Field::Id id, const std::vector<std::string>& vals) +{ + if (vals.empty()) + return; + + const auto field{field_from_id(id)}; + if (field.is_value()) + xdoc_.add_value(field.value_no(), Mu::join(vals, SepaChar1)); + + if (field.is_searchable()) + std::for_each(vals.begin(), vals.end(), + [&](const auto& val) { + add_search_term(xdoc_, field, val); }); + + if (field.include_in_sexp()) { + Sexp::List elms; + for(auto&& val: vals) + elms.add(Sexp::make_string(val)); + sexp_list().add_prop(make_prop_name(field), + Sexp::make_list(std::move(elms))); + } +} + + +std::vector<std::string> +Document::string_vec_value(Field::Id field_id) const noexcept +{ + return Mu::split(string_value(field_id), SepaChar1); +} + +static Sexp +make_contacts_sexp(const Contacts& contacts) +{ + Sexp::List clist; + + seq_for_each(contacts, [&](auto&& c) { + if (!c.name.empty()) + clist.add(Sexp::make_prop_list( + ":name", Sexp::make_string(c.name), + ":email", Sexp::make_string(c.email))); + else + clist.add(Sexp::make_prop_list( + ":email", Sexp::make_string(c.email))); + }); + + return Sexp::make_list(std::move(clist)); +} + +void +Document::add(Field::Id id, const Contacts& contacts) +{ + if (contacts.empty()) + return; + + const auto field{field_from_id(id)}; + std::vector<std::string> cvec; + + const std::string sepa2(1, SepaChar2); + + Xapian::TermGenerator termgen; + termgen.set_document(xdoc_); + + for (auto&& contact: contacts) { + + const auto cfield_id{contact.field_id()}; + if (!cfield_id || *cfield_id != id) + continue; + + const auto e{contact.email}; + xdoc_.add_term(field.xapian_term(e)); + + /* allow searching for address components, too */ + const auto atpos = e.find('@'); + if (atpos != std::string::npos && atpos < e.size() - 1) { + xdoc_.add_term(field.xapian_term(e.substr(0, atpos))); + xdoc_.add_term(field.xapian_term(e.substr(atpos + 1))); + } + + if (!contact.name.empty()) + termgen.index_text(utf8_flatten(contact.name), 1, + field.xapian_term()); + cvec.emplace_back(contact.email + sepa2 + contact.name); + } + + if (!cvec.empty()) + xdoc_.add_value(field.value_no(), join(cvec, SepaChar1)); + + if (field.include_in_sexp()) + sexp_list().add_prop(make_prop_name(field), + make_contacts_sexp(contacts)); + +} + +Contacts +Document::contacts_value(Field::Id id) const noexcept +{ + const auto vals{string_vec_value(id)}; + Contacts contacts; + contacts.reserve(vals.size()); + + const auto ctype{contact_type_from_field_id(id)}; + if (G_UNLIKELY(!ctype)) { + g_critical("invalid field-id for contact-type: <%zu>", + static_cast<size_t>(id)); + return {}; + } + + for (auto&& s: vals) { + + const auto pos = s.find(SepaChar2); + if (G_UNLIKELY(pos == std::string::npos)) { + g_critical("invalid contact data '%s'", s.c_str()); + break; + } + + contacts.emplace_back(s.substr(0, pos), s.substr(pos + 1), *ctype); + } + + return contacts; +} + +void +Document::add_extra_contacts(const std::string& propname, const Contacts& contacts) +{ + if (!contacts.empty()) + sexp_list().add_prop(std::string{propname}, + make_contacts_sexp(contacts)); +} + + +static Sexp +make_emacs_time_sexp(::time_t t) +{ + Sexp::List dlist; + + dlist.add(Sexp::make_number(static_cast<unsigned>(t >> 16))); + dlist.add(Sexp::make_number(static_cast<unsigned>(t & 0xffff))); + dlist.add(Sexp::make_number(0)); + + return Sexp::make_list(std::move(dlist)); +} + +void +Document::add(Field::Id id, int64_t val) +{ + /* + * Xapian stores everything (incl. numbers) as strings. + * + * we comply, by storing a number a base-16 and prefixing with 'f' + + * length; such that the strings are sorted in the numerical order. + */ + + const auto field{field_from_id(id)}; + + if (field.is_value()) + xdoc_.add_value(field.value_no(), to_lexnum(val)); + + if (field.include_in_sexp()) { + if (field.is_time_t()) + sexp_list().add_prop(make_prop_name(field), + make_emacs_time_sexp(val)); + else + sexp_list().add_prop(make_prop_name(field), + Sexp::make_number(val)); + } +} + +int64_t +Document::integer_value(Field::Id field_id) const noexcept +{ + if (auto&& v{string_value(field_id)}; v.empty()) + return 0; + else + return from_lexnum(v); +} + +void +Document::add(Priority prio) +{ + constexpr auto field{field_from_id(Field::Id::Priority)}; + + xdoc_.add_value(field.value_no(), std::string(1, to_char(prio))); + xdoc_.add_boolean_term(field.xapian_term(to_char(prio))); + + if (field.include_in_sexp()) + sexp_list().add_prop(make_prop_name(field), + Sexp::make_symbol_sv(priority_name(prio))); +} + +Priority +Document::priority_value() const noexcept +{ + const auto val{string_value(Field::Id::Priority)}; + return priority_from_char(val.empty() ? 'n' : val[0]); +} + +void +Document::add(Flags flags) +{ + constexpr auto field{field_from_id(Field::Id::Flags)}; + + Sexp::List flaglist; + xdoc_.add_value(field.value_no(), to_lexnum(static_cast<int64_t>(flags))); + flag_infos_for_each([&](auto&& flag_info) { + auto term=[&](){return field.xapian_term(flag_info.shortcut_lower());}; + if (any_of(flag_info.flag & flags)) { + xdoc_.add_boolean_term(term()); + flaglist.add(Sexp::make_symbol_sv(flag_info.name)); + } + }); + + if (field.include_in_sexp()) + sexp_list().add_prop(make_prop_name(field), + Sexp::make_list(std::move(flaglist))); +} + + +Sexp::List& +Document::sexp_list() +{ + /* perhaps we need get the sexp_ from the document first? */ + if (sexp_list_.empty()) { + const auto str{xdoc_.get_data()}; + if (!str.empty()) { + Sexp sexp{Sexp::make_parse(str)}; + sexp_list_ = sexp.list(); + } + } + + return sexp_list_; +} + +std::string +Document::cached_sexp() const +{ + return xdoc_.get_data(); +} + +void +Document::update_cached_sexp(void) +{ + if (sexp_list_.empty()) + return; /* nothing to do; i.e. the exisiting sexp is still up to + * date */ + xdoc_.set_data(Sexp::make_list(Sexp::List{sexp_list()}).to_sexp_string()); +} + +Flags +Document::flags_value() const noexcept +{ + return static_cast<Flags>(integer_value(Field::Id::Flags)); +} + +void +Document::remove(Field::Id field_id) +{ + const auto field{field_from_id(field_id)}; + const auto pfx{field.xapian_prefix()}; + + xapian_try([&]{ + + if (auto&& val{xdoc_.get_value(field.value_no())}; !val.empty()) { + // g_debug("removing value<%u>: '%s'", field.value_no(), + // val.c_str()); + xdoc_.remove_value(field.value_no()); + } + + std::vector<std::string> kill_list; + for (auto&& it = xdoc_.termlist_begin(); + it != xdoc_.termlist_end(); ++it) { + const auto term{*it}; + if (!term.empty() && term.at(0) == pfx) + kill_list.emplace_back(term); + } + + for (auto&& term: kill_list) { + // g_debug("removing term '%s'", term.c_str()); + try { + xdoc_.remove_term(term); + } catch(const Xapian::InvalidArgumentError& xe) { + g_critical("failed to remove '%s'", term.c_str()); + } + } + }); + +} + + +#ifdef BUILD_TESTS + +#include "utils/mu-test-utils.hh" + +#define assert_same_contact(C1,C2) do { \ + g_assert_cmpstr(C1.email.c_str(),==,C2.email.c_str()); \ + g_assert_cmpstr(C2.name.c_str(),==,C2.name.c_str()); \ + } while (0) + +#define assert_same_contacts(CV1,CV2) do { \ + g_assert_cmpuint(CV1.size(),==,CV2.size()); \ + for (auto i = 0U; i != CV1.size(); ++i) \ + assert_same_contact(CV1[i], CV2[i]); \ + } while(0) + + + +static const Contacts test_contacts = {{ + Contact{"john@example.com", "John", Contact::Type::Bcc}, + Contact{"ringo@example.com", "Ringo", Contact::Type::Bcc}, + Contact{"paul@example.com", "Paul", Contact::Type::Cc}, + Contact{"george@example.com", "George", Contact::Type::Cc}, + Contact{"james@example.com", "James", Contact::Type::From}, + Contact{"lars@example.com", "Lars", Contact::Type::To}, + Contact{"kirk@example.com", "Kirk", Contact::Type::To}, + Contact{"jason@example.com", "Jason", Contact::Type::To} + }}; + +static void +test_bcc() +{ + { + Document doc; + doc.add(Field::Id::Bcc, test_contacts); + + Contacts expected_contacts = {{ + Contact{"john@example.com", "John", + Contact::Type::Bcc}, + Contact{"ringo@example.com", "Ringo", + Contact::Type::Bcc}, + }}; + const auto actual_contacts = doc.contacts_value(Field::Id::Bcc); + assert_same_contacts(expected_contacts, actual_contacts); + } + + { + Document doc; + Contacts contacts = {{ + Contact{"john@example.com", "John Lennon", + Contact::Type::Bcc}, + Contact{"ringo@example.com", "Ringo", + Contact::Type::Bcc}, + }}; + doc.add(Field::Id::Bcc, contacts); + + TempDir tempdir; + auto db = Xapian::WritableDatabase(tempdir.path()); + db.add_document(doc.xapian_document()); + + auto contacts2 = doc.contacts_value(Field::Id::Bcc); + assert_same_contacts(contacts, contacts2); + } + +} + +static void +test_cc() +{ + Document doc; + doc.add(Field::Id::Cc, test_contacts); + + Contacts expected_contacts = {{ + Contact{"paul@example.com", "Paul", Contact::Type::Cc}, + Contact{"george@example.com", "George", Contact::Type::Cc} + }}; + const auto actual_contacts = doc.contacts_value(Field::Id::Cc); + + assert_same_contacts(expected_contacts, actual_contacts); +} + + +static void +test_from() +{ + Document doc; + doc.add(Field::Id::From, test_contacts); + + Contacts expected_contacts = {{ + Contact{"james@example.com", "James", Contact::Type::From}, + }}; + const auto actual_contacts = doc.contacts_value(Field::Id::From); + + assert_same_contacts(expected_contacts, actual_contacts); +} + +static void +test_to() +{ + Document doc; + doc.add(Field::Id::To, test_contacts); + + Contacts expected_contacts = {{ + Contact{"lars@example.com", "Lars", Contact::Type::To}, + Contact{"kirk@example.com", "Kirk", Contact::Type::To}, + Contact{"jason@example.com", "Jason", Contact::Type::To} + }}; + const auto actual_contacts = doc.contacts_value(Field::Id::To); + + assert_same_contacts(expected_contacts, actual_contacts); +} + + +static void +test_size() +{ + { + Document doc; + doc.add(Field::Id::Size, 12345); + g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,12345); + } + + { + Document doc; + g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,0); + } +} + + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/message/document/bcc", test_bcc); + g_test_add_func("/message/document/cc", test_cc); + g_test_add_func("/message/document/from", test_from); + g_test_add_func("/message/document/to", test_to); + + g_test_add_func("/message/document/size", test_size); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-document.hh b/lib/message/mu-document.hh new file mode 100644 index 0000000..9c923a1 --- /dev/null +++ b/lib/message/mu-document.hh @@ -0,0 +1,239 @@ +/** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_DOCUMENT_HH__ +#define MU_DOCUMENT_HH__ + +#include <xapian.h> +#include <utility> +#include <string> +#include <vector> +#include "utils/mu-xapian-utils.hh" + +#include "mu-fields.hh" +#include "mu-priority.hh" +#include "mu-flags.hh" +#include "mu-contact.hh" +#include <utils/mu-option.hh> +#include <utils/mu-sexp.hh> + +namespace Mu { + +/** + * A Document describes the information about a message that is + * or can be stored in the database. + * + */ +class Document { +public: + /** + * Construct a message for a new Xapian Document + * + */ + Document() {} + + /** + * Construct a message document based on on existing Xapian document. + * + * @param doc + */ + Document(const Xapian::Document& doc): xdoc_{doc} {} + + /** + * Get a reference to the underlying Xapian document. + * + */ + const Xapian::Document& xapian_document() const { return xdoc_; } + + /** + * Get the doc-id for this document + * + * @return the docid + */ + Xapian::docid docid() const { return xdoc_.get_docid(); } + + /* + * updating a document with terms & values + */ + + /** + * Add a string value to the document + * + * @param field_id field id + * @param val string value + */ + void add(Field::Id field_id, const std::string& val); + + /** + * Add a string-vec value to the document, if non-empty + * + * @param field_id field id + * @param val string-vec value + */ + void add(Field::Id field_id, const std::vector<std::string>& vals); + + + /** + * Add message-contacts to the document, if non-empty + * + * @param field_id field id + * @param contacts message contacts + */ + void add(Field::Id id, const Contacts& contacts); + + /** + * Add some extra contacts with the given propname; this is useful for + * ":reply-to" and ":list-post" which don't have a Field::Id and are + * only present in the sexp, not in the terms/values + * + * @param propname property name (e.g.,. ":reply-to") + * @param contacts contacts for this property. + */ + void add_extra_contacts(const std::string& propname, + const Contacts& contacts); + + /** + * Add an integer value to the document + * + * @param field_id field id + * @param val integer value + */ + void add(Field::Id field_id, int64_t val); + + /** + * Add a message priority to the document + * + * @param prio priority + */ + void add(Priority prio); + + + /** + * Add message flags to the document + * + * @param flags mesage flags. + */ + void add(Flags flags); + + /** + * Remove values and terms for some field. + * + * @param field_id + */ + void remove(Field::Id field_id); + + /** + * Update the cached sexp from the sexp_list_ + */ + void update_cached_sexp(); + + /** + * Get the cached s-expression + * + * @return a string + */ + std::string cached_sexp() const; + + /** + * Get the cached s-expressionl useful for changing + * it (call update_sexp_cache() when done) + * + * @return the cache s-expression + */ + Sexp::List& sexp_list(); + + /** + * Generically adds an optional value, if set, to the document + * + * @param id the field 0d + * @param an optional value + */ + template<typename T> void add(Field::Id id, const Option<T>& val) { + if (val) + add(id, val.value()); + } + + /* + * Retrieving values + */ + + /** + * Get a message-field as a string-value + * + * @param field_id id of the field to get. + * + * @return a string (empty if not found) + */ + std::string string_value(Field::Id field_id) const noexcept { + return xapian_try([&]{ + return xdoc_.get_value(field_from_id(field_id).value_no()); + }, std::string{}); + } + /** + * Get a vec of string values. + * + * @param field_id id of the field to get + * + * @return a string list + */ + std::vector<std::string> string_vec_value(Field::Id field_id) const noexcept; + + + /** + * Get an integer value + * + * @param field_id id of the field to get + * + * @return an integer or 0 if not found. + */ + int64_t integer_value(Field::Id field_id) const noexcept; + + + /** + * Get contacts + * + * @param field_id id of the contacts field to get + * + * @return a contacts list + */ + Contacts contacts_value(Field::Id id) const noexcept; + + /** + * Get the priority + * + * @return the message priority + */ + Priority priority_value() const noexcept; + + /** + * Get the message flags + * + * + * @return flags + */ + Flags flags_value() const noexcept; + +private: + Xapian::Document xdoc_; + Sexp::List sexp_list_; + +}; + +} // namepace Mu + +#endif /* MU_DOCUMENT_HH__ */ diff --git a/lib/message/mu-fields.cc b/lib/message/mu-fields.cc new file mode 100644 index 0000000..6727e57 --- /dev/null +++ b/lib/message/mu-fields.cc @@ -0,0 +1,200 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-fields.hh" +#include "mu-flags.hh" + +#include "utils/mu-test-utils.hh" + +using namespace Mu; + +std::string +Field::xapian_term(const std::string& s) const +{ + const auto start{std::string(1U, xapian_prefix())}; + if (const auto& size = s.size(); size == 0) + return start; + + std::string res{start}; + res.reserve(s.size() + 10); + + /* slightly optimized common pure-ascii. */ + if (G_LIKELY(g_str_is_ascii(s.c_str()))) { + res += s; + for (auto i = 1; res[i]; ++i) + res[i] = g_ascii_tolower(res[i]); + } else + res += utf8_flatten(s); + + if (G_UNLIKELY(res.size() > MaxTermLength)) + res.erase(MaxTermLength); + + return res; +} + +/** + * compile-time checks + */ +constexpr bool +validate_field_ids() +{ + for (auto id = 0U; id != Field::id_size(); ++id) { + const auto field_id = static_cast<Field::Id>(id); + if (field_from_id(field_id).id != field_id) + return false; + } + return true; +} + +constexpr bool +validate_field_shortcuts() +{ +#ifdef BUILD_TESTS + std::array<size_t, 26> no_dups = {0}; +#endif /*BUILD_TESTS*/ + for (auto id = 0U; id != Field::id_size(); ++id) { + const auto field_id = static_cast<Field::Id>(id); + const auto shortcut = field_from_id(field_id).shortcut; + if (shortcut != 0 && + (shortcut < 'a' || shortcut > 'z')) + return false; +#ifdef BUILD_TESTS + if (shortcut != 0) { + if (++no_dups[static_cast<size_t>(shortcut-'a')] > 1) { + g_critical("shortcut '%c' is duplicated", + shortcut); + return false; + } + } +#endif + } + + return true; +} + + +constexpr /*static*/ bool +validate_field_flags() +{ + for (auto&& field: Fields) { + /* - A field has at most one of Indexable, HasTerms, IsXapianBoolean and + IsContact. */ + size_t flagnum{}; + + if (field.is_indexable_term()) + ++flagnum; + if (field.is_boolean_term()) + ++flagnum; + if (field.is_normal_term()) + ++flagnum; + + if (flagnum > 1) { + //g_warning("invalid field %*.s", STR_V(field.name)); + return false; + } + } + + return true; +} + +/* + * tests... also build as runtime-tests, so we can get coverage info + */ +#ifdef BUILD_TESTS +#define static_assert g_assert_true +#endif /*BUILD_TESTS*/ + + +[[maybe_unused]] +static void +test_ids() +{ + static_assert(validate_field_ids()); +} + +[[maybe_unused]] +static void +test_shortcuts() +{ + static_assert(validate_field_shortcuts()); +} + +[[maybe_unused]] +static void +test_prefix() +{ + static_assert(field_from_id(Field::Id::Subject).xapian_prefix() == 'S'); + static_assert(field_from_id(Field::Id::XBodyHtml).xapian_prefix() == 0); +} + +[[maybe_unused]] +static void +test_field_flags() +{ + static_assert(validate_field_flags()); +} + +#ifdef BUILD_TESTS + + +static void +test_field_from_name() +{ + g_assert_true(field_from_name("s")->id == Field::Id::Subject); + g_assert_true(field_from_name("subject")->id == Field::Id::Subject); + g_assert_false(!!field_from_name("8")); + g_assert_false(!!field_from_name("")); + + g_assert_true(field_from_name("").value_or(field_from_id(Field::Id::Bcc)).id == + Field::Id::Bcc); +} + + +static void +test_xapian_term() +{ + using namespace std::string_literals; + using namespace std::literals; + + assert_equal(field_from_id(Field::Id::Subject).xapian_term(""s), "S"); + assert_equal(field_from_id(Field::Id::Subject).xapian_term("boo"s), "Sboo"); + + assert_equal(field_from_id(Field::Id::From).xapian_term('x'), "Fx"); + assert_equal(field_from_id(Field::Id::To).xapian_term("boo"sv), "Tboo"); + + auto s1 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength - 1, 'x')); + auto s2 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength, 'x')); + g_assert_cmpuint(s1.length(), ==, s2.length()); +} + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_add_func("/message/fields/ids", test_ids); + g_test_add_func("/message/fields/shortcuts", test_shortcuts); + g_test_add_func("/message/fields/from-name", test_field_from_name); + g_test_add_func("/message/fields/prefix", test_prefix); + g_test_add_func("/message/fields/xapian-term", test_xapian_term); + g_test_add_func("/message/fields/flags", test_field_flags); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-fields.hh b/lib/message/mu-fields.hh new file mode 100644 index 0000000..22b486b --- /dev/null +++ b/lib/message/mu-fields.hh @@ -0,0 +1,546 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_FIELDS_HH__ +#define MU_FIELDS_HH__ + +#include <cstdint> +#include <string_view> +#include <algorithm> +#include <array> +#include <xapian.h> +#include <utils/mu-utils.hh> +#include <utils/mu-option.hh> + +namespace Mu { + +// Xapian does not like terms much longer than this +constexpr auto MaxTermLength = 240; +// http://article.gmane.org/gmane.comp.search.xapian.general/3656 */ + +struct Field { + /** + * Field Ids. + * + * Note, the Ids are also used as indices in the Fields array, + * so their numerical values must be 0...Count. + * + */ + enum struct Id { + Bcc = 0, /**< Blind Carbon-Copy */ + BodyText, /**< Text body */ + Cc, /**< Carbon-Copy */ + Changed, /**< Last change time (think 'ctime') */ + Date, /**< Message date */ + EmbeddedText, /**< Embedded text in message */ + File, /**< Filename */ + Flags, /**< Message flags */ + From, /**< Message sender */ + Maildir, /**< Maildir path */ + MailingList, /**< Mailing list */ + MessageId, /**< Message Id */ + MimeType, /**< MIME-Type */ + Path, /**< File-system Path */ + Priority, /**< Message priority */ + References, /**< All references (incl. Reply-To:) */ + Size, /**< Message size (in bytes) */ + Subject, /**< Message subject */ + Tags, /**< Message Tags */ + ThreadId, /**< Thread Id */ + To, /**< To: recipient */ + /* + * <private> + */ + XBodyHtml, /**< HTML Body */ + + _count_ /**< Number of FieldIds */ + }; + + /** + * Get the number of Id values. + * + * @return the number. + */ + static constexpr size_t id_size() + { + return static_cast<size_t>(Id::_count_); + } + + constexpr Xapian::valueno value_no() const { + return static_cast<Xapian::valueno>(id); + } + + /** + * Field types + * + */ + enum struct Type { + String, /**< String */ + StringList, /**< List of strings */ + ContactList, /**< List of contacts */ + ByteSize, /**< Size in bytes */ + TimeT, /**< A time_t value */ + Integer, /**< An integer */ + }; + + constexpr bool is_string() const { return type == Type::String; } + constexpr bool is_string_list() const { return type == Type::StringList; } + constexpr bool is_byte_size() const { return type == Type::ByteSize; } + constexpr bool is_time_t() const { return type == Type::TimeT; } + constexpr bool is_integer() const { return type == Type::Integer; } + constexpr bool is_numerical() const { return is_byte_size() || is_time_t() || is_integer(); } + + /** + * Field flags + * note: the differences for our purposes between a xapian field and a + * term: - there is only a single value for some item in per document + * (msg), ie. one value containing the list of To: addresses - there + * can be multiple terms, each containing e.g. one of the To: + * addresses - searching uses terms, but to display some field, it + * must be in the value (at least when using MuMsgIter) + * + * Rules (build-time enforced): + * - A field has at most one of Indexable, HasTerms, IsXapianBoolean and IsContact. + */ + + enum struct Flag { + /* + * Different kind of terms; at most one is true, + * and cannot be combined with IsContact. Compile-time enforced. + */ + NormalTerm = 1 << 0, + /**< Field is a searchable term */ + BooleanTerm = 1 << 1, + /**< Field is a boolean search-term (i.e. at most one per message); + * wildcards do not work */ + IndexableTerm = 1 << 2, + /**< Field has indexable text as term */ + /* + * Contact flag cannot be combined with any of the term flags. + * This is compile-time enforced. + */ + Contact = 1 << 10, + /**< field contains one or more e-mail-addresses */ + Value = 1 << 11, + /**< Field value is stored (so the literal value can be retrieved) */ + + Range = 1 << 21, + + IncludeInSexp = 1 << 24, + /**< whether to include this field in the cached sexp. */ + + /**< whether this is a range field (e.g., date, size)*/ + Internal = 1 << 26 + }; + + constexpr bool any_of(Flag some_flag) const{ + return (static_cast<int>(some_flag) & static_cast<int>(flags)) != 0; + } + + constexpr bool is_indexable_term() const { return any_of(Flag::IndexableTerm); } + constexpr bool is_boolean_term() const { return any_of(Flag::BooleanTerm); } + constexpr bool is_normal_term() const { return any_of(Flag::NormalTerm); } + constexpr bool is_searchable() const { return is_indexable_term() || + is_boolean_term() || + is_normal_term(); } + + constexpr bool is_value() const { return any_of(Flag::Value); } + constexpr bool is_internal() const { return any_of(Flag::Internal); } + + constexpr bool is_contact() const { return any_of(Flag::Contact); } + constexpr bool is_range() const { return any_of(Flag::Range); } + + constexpr bool include_in_sexp() const { return any_of(Flag::IncludeInSexp);} + + /** + * Field members + * + */ + Id id; /**< Id of the message field */ + Type type; /**< Type of the message field */ + std::string_view name; /**< Name of the message field */ + std::string_view alias; /**< Alternative name for the message field */ + std::string_view description; /**< Decription of the message field */ + std::string_view example_query; /**< Example query */ + char shortcut; /**< Shortcut for the message field; a..z */ + Flag flags; /**< Flags */ + + /** + * Convenience / helpers + * + */ + + constexpr char xapian_prefix() const + { /* xapian uses uppercase shortcuts; toupper is not constexpr */ + return shortcut == 0 ? 0 : shortcut - ('a' - 'A'); + } + + /** + * Get the xapian term; truncated to MaxTermLength and + * utf8-flattened. + * + * @param s + * + * @return the xapian term + */ + std::string xapian_term(const std::string& s="") const; + std::string xapian_term(std::string_view sv) const { + return xapian_term(std::string{sv}); + } + std::string xapian_term(char c) const { + return xapian_term(std::string(1, c)); + } +}; + +MU_ENABLE_BITOPS(Field::Flag); + +/** + * Sequence of _all_ message fields + */ +static constexpr std::array<Field, Field::id_size()> + Fields = { + { + { + Field::Id::Bcc, + Field::Type::ContactList, + "bcc", {}, + "Blind carbon-copy recipient", + "bcc:foo@example.com", + 'h', + Field::Flag::Contact | + Field::Flag::Value | + Field::Flag::IncludeInSexp | + Field::Flag::IndexableTerm, + + }, + { + Field::Id::BodyText, + Field::Type::String, + "body", {}, + "Message plain-text body", + "body:capybara", + 'b', + Field::Flag::IndexableTerm, + }, + { + Field::Id::Cc, + Field::Type::ContactList, + "cc", {}, + "Carbon-copy recipient", + "cc:quinn@example.com", + 'c', + Field::Flag::Contact | + Field::Flag::Value | + Field::Flag::IncludeInSexp | + Field::Flag::IndexableTerm, + }, + + { + Field::Id::Changed, + Field::Type::TimeT, + "changed", {}, + "Last change time", + "changed:30M..", + 'k', + Field::Flag::Value | + Field::Flag::Range | + Field::Flag::IncludeInSexp + }, + { + Field::Id::Date, + Field::Type::TimeT, + "date", {}, + "Message date", + "date:20220101..20220505", + 'd', + Field::Flag::Value | + Field::Flag::Range | + Field::Flag::IncludeInSexp + }, + { + Field::Id::EmbeddedText, + Field::Type::String, + "embed", {}, + "Embedded text", + "embed:war OR embed:peace", + 'e', + Field::Flag::IndexableTerm + }, + { + Field::Id::File, + Field::Type::String, + "file", {}, + "Attachment file name", + "file:/image\\.*.jpg/", + 'j', + Field::Flag::BooleanTerm + }, + { + Field::Id::Flags, + Field::Type::Integer, + "flags", "flag", + "Message properties", + "flag:unread AND flag:personal", + 'g', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::From, + Field::Type::ContactList, + "from", {}, + "Message sender", + "from:jimbo", + 'f', + Field::Flag::Contact | + Field::Flag::Value | + Field::Flag::IncludeInSexp | + Field::Flag::IndexableTerm, + }, + { + Field::Id::Maildir, + Field::Type::String, + "maildir", {}, + "Maildir path for message", + "maildir:/private/archive", + 'm', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::MailingList, + Field::Type::String, + "list", {}, + "Mailing list (List-Id:)", + "list:mu-discuss.example.com", + 'v', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::MessageId, + Field::Type::String, + "message-id", "msgid", + "Message-Id", + "msgid:abc@123", + 'i', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::MimeType, + Field::Type::String, + "mime", "mime-type", + "Attachment MIME-type", + "mime:image/jpeg", + 'y', + Field::Flag::BooleanTerm + }, + { + Field::Id::Path, + Field::Type::String, + "path", {}, + "File system path to message", + "path:/a/b/Maildir/cur/msg:2,S", + 'l', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::Priority, + Field::Type::Integer, + "priority", "prio", + "Priority", + "prio:high", + 'p', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::References, + Field::Type::StringList, + "references", {}, + "References to related messages", + {}, + 'r', + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::Size, + Field::Type::ByteSize, + "size", {}, + "Message size in bytes", + "size:1M..5M", + 'z', + Field::Flag::Value | + Field::Flag::Range | + Field::Flag::IncludeInSexp + }, + { + Field::Id::Subject, + Field::Type::String, + "subject", {}, + "Message subject", + "subject:wombat", + 's', + Field::Flag::Value | + Field::Flag::IndexableTerm | + Field::Flag::IncludeInSexp + }, + { + Field::Id::Tags, + Field::Type::StringList, + "tags", "tag", + "Message tags", + "tag:projectx", + 'x', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp + }, + { + Field::Id::ThreadId, + Field::Type::String, + "thread", {}, + "Thread a message belongs to", + {}, + 'w', + Field::Flag::BooleanTerm | + Field::Flag::Value + }, + { + Field::Id::To, + Field::Type::ContactList, + "to", {}, + "Message recipient", + "to:flimflam@example.com", + 't', + Field::Flag::Contact | + Field::Flag::Value | + Field::Flag::IncludeInSexp | + Field::Flag::IndexableTerm, + }, + + /* internal */ + { + Field::Id::XBodyHtml, + Field::Type::String, + "htmlbody", {}, + "Message html body", + {}, + {}, + Field::Flag::Internal + }, + }}; + +/* + * Convenience + */ + +/** + * Get the message field for the given Id. + * + * @param id of the message field + * + * @return ref of the message field. + */ +constexpr const Field& +field_from_id(Field::Id id) +{ + return Fields.at(static_cast<size_t>(id)); +} + +/** + * Invoke func for each message-field + * + * @param func some callable + */ +template <typename Func> +constexpr void field_for_each(Func&& func) { + for (const auto& field: Fields) + func(field); +} + +/** + * Find a message field that satisfies some predicate + * + * @param pred the predicate (a callable) + * + * @return a message-field id, or nullopt if not found. + */ +template <typename Pred> +constexpr Option<Field> field_find_if(Pred&& pred) { + for (auto&& field: Fields) + if (pred(field)) + return field; + return Nothing; +} + +/** + * Get the the message-field id for the given name or shortcut + * + * @param name_or_shortcut + * + * @return the message-field-id or nullopt. + */ +static inline +Option<Field> field_from_shortcut(char shortcut) { + return field_find_if([&](auto&& field){ + return field.shortcut == shortcut; + }); +} +static inline +Option<Field> field_from_name(const std::string& name) { + switch(name.length()) { + case 0: + return Nothing; + case 1: + return field_from_shortcut(name[0]); + default: + return field_find_if([&](auto&& field){ + return name == field.name || name == field.alias; + }); + } +} + +/** + * Get the Field::Id for some number, or nullopt if it does not match + * + * @param id an id number + * + * @return Field::Id or nullopt + */ +static inline +Option<Field> field_from_number(size_t id) +{ + if (id >= static_cast<size_t>(Field::Id::_count_)) + return Nothing; + else + return field_from_id(static_cast<Field::Id>(id)); +} + +} // namespace Mu +#endif /* MU_FIELDS_HH__ */ diff --git a/lib/message/mu-flags.cc b/lib/message/mu-flags.cc new file mode 100644 index 0000000..549fa2e --- /dev/null +++ b/lib/message/mu-flags.cc @@ -0,0 +1,170 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +/* + * implementation is almost completely in the header; here we just add some + * compile-time tests. + */ + +#include "mu-flags.hh" + +using namespace Mu; + +std::string +Mu::to_string(Flags flags) +{ + std::string str; + + for (auto&& info: AllMessageFlagInfos) + if (any_of(info.flag & flags)) + str+=info.shortcut; + + return str; +} + + +/* + * flags & flag-info + */ +constexpr bool +validate_message_info_flags() +{ + for (auto id = 0U; id != AllMessageFlagInfos.size(); ++id) { + const auto flag = static_cast<Flags>(1 << id); + if (flag != AllMessageFlagInfos[id].flag) + return false; + } + return true; +} + + +/* + * tests... also build as runtime-tests, so we can get coverage info + */ +#ifdef BUILD_TESTS +#define static_assert g_assert_true +#endif /*BUILD_TESTS*/ + +[[maybe_unused]] static void +test_basic() +{ + static_assert(AllMessageFlagInfos.size() == + __builtin_ctz(static_cast<unsigned>(Flags::_final_))); + static_assert(validate_message_info_flags()); + + static_assert(!!flag_info(Flags::Encrypted)); + static_assert(!flag_info(Flags::None)); + static_assert(!flag_info(static_cast<Flags>(0))); + static_assert(!flag_info(static_cast<Flags>(1<<AllMessageFlagInfos.size()))); +} + +/* + * flag_info + */ +[[maybe_unused]] static void +test_flag_info() +{ + static_assert(flag_info('D')->flag == Flags::Draft); + static_assert(flag_info('l')->flag == Flags::MailingList); + static_assert(!flag_info('y')); + + static_assert(flag_info("trashed")->flag == Flags::Trashed); + static_assert(flag_info("attach")->flag == Flags::HasAttachment); + static_assert(!flag_info("fnorb")); + + + static_assert(flag_info('D')->shortcut_lower() == 'd'); + static_assert(flag_info('u')->shortcut_lower() == 'u'); +} + +/* + * flags_from_expr + */ +[[maybe_unused]] static void +test_flags_from_expr() +{ + static_assert(flags_from_absolute_expr("SRP").value() == + (Flags::Seen | Flags::Replied | Flags::Passed)); + static_assert(flags_from_absolute_expr("Faul").value() == + (Flags::Flagged | Flags::Unread | + Flags::HasAttachment | Flags::MailingList)); + + static_assert(!flags_from_absolute_expr("DRT?")); + static_assert(flags_from_absolute_expr("DRT?", true/*ignore invalid*/).value() == + (Flags::Draft | Flags::Replied | + Flags::Trashed)); + static_assert(flags_from_absolute_expr("DFPNxulabcdef", true/*ignore invalid*/).value() == + (Flags::Draft|Flags::Flagged|Flags::Passed| + Flags::New | Flags::Encrypted | + Flags::Unread | Flags::MailingList | Flags::Calendar | + Flags::HasAttachment)); +} + + +/* + * flags_from_delta_expr + */ +[[maybe_unused]] static void +test_flags_from_delta_expr() +{ + static_assert(flags_from_delta_expr( + "+S-u-N", Flags::New|Flags::Unread).value() == + Flags::Seen); + static_assert(flags_from_delta_expr("+R+P-F", Flags::Seen).value() == + (Flags::Seen|Flags::Passed|Flags::Replied)); + /* '-B' is invalid */ + static_assert(!flags_from_delta_expr("+R+P-B", Flags::Seen)); + /* '-B' is invalid, but ignore invalid */ + static_assert(flags_from_delta_expr("+R+P-B", Flags::Seen, true) == + (Flags::Replied|Flags::Passed|Flags::Seen)); + static_assert(flags_from_delta_expr("+F+T-S", Flags::None, true).value() == + (Flags::Flagged|Flags::Trashed)); +} + +/* + * flags_filter + */ +[[maybe_unused]] static void +test_flags_filter() +{ + static_assert(flags_filter(flags_from_absolute_expr( + "DFPNxulabcdef", true/*ignore invalid*/).value(), + MessageFlagCategory::Mailfile) == + (Flags::Draft|Flags::Flagged|Flags::Passed)); +} + + +#ifdef BUILD_TESTS +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/message/flags/basic", test_basic); + g_test_add_func("/message/flags/flag-info", test_flag_info); + g_test_add_func("/message/flags/flags-from-absolute-expr", + test_flags_from_expr); + g_test_add_func("/message/flags/flags-from-delta-expr", + test_flags_from_delta_expr); + g_test_add_func("/message/flags/flags-filter", + test_flags_filter); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-flags.hh b/lib/message/mu-flags.hh new file mode 100644 index 0000000..0ac6496 --- /dev/null +++ b/lib/message/mu-flags.hh @@ -0,0 +1,352 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_FLAGS_HH__ +#define MU_FLAGS_HH__ + +#include <algorithm> +#include <string_view> +#include <array> +#include <utils/mu-utils.hh> +#include <utils/mu-option.hh> + +namespace Mu { + +enum struct Flags { + None = 0, /**< No flags */ + /** + * next 6 are seen in the file-info part of maildir message file + * names, ie., in a name like "1234345346:2,<fileinfo>", + * <fileinfo> consists of zero or more of the following + * characters (in ascii order) + */ + Draft = 1 << 0, /**< A draft message */ + Flagged = 1 << 1, /**< A flagged message */ + Passed = 1 << 2, /**< A passed (forwarded) message */ + Replied = 1 << 3, /**< A replied message */ + Seen = 1 << 4, /**< A seen (read) message */ + Trashed = 1 << 5, /**< A trashed message */ + + /** + * decides on cur/ or new/ in the maildir + */ + New = 1 << 6, /**< A new message */ + + /** + * content flags -- not visible in the filename, but used for + * searching + */ + Signed = 1 << 7, /**< Cryptographically signed */ + Encrypted = 1 << 8, /**< Encrypted */ + HasAttachment = 1 << 9, /**< Has an attachment */ + + Unread = 1 << 10, /**< Unread; pseudo-flag, only for queries, so we can + * search for flag:unread, which is equivalent to + * 'flag:new OR NOT flag:seen' */ + /** + * other content flags + */ + MailingList = 1 << 11, /**< A mailing-list message */ + Personal = 1 << 12, /**< A personal message (i.e., at least one of the + * contact fields contains a personal address) */ + Calendar = 1 << 13, /**< A calendar invitation */ + /* + * <private> + */ + _final_ = 1 << 14 +}; +MU_ENABLE_BITOPS(Flags); + +/** + * Message flags category + * + */ +enum struct MessageFlagCategory { + None, /**< Nothing */ + Mailfile, /**< Flag for a message file */ + Maildir, /**< Flag for message file's location */ + Content, /**< Message content flag */ + Pseudo /**< Pseudo flag */ +}; + +/** + * Info about invidual message flags + * + */ +struct MessageFlagInfo { + + Flags flag; /**< The message flag */ + char shortcut; /**< Shortcut character; + * tolower(shortcut) must be + * unique for all flags */ + std::string_view name; /**< Name of the flag */ + MessageFlagCategory category; /**< Flag category */ + std::string_view description; /**< Description */ + + /** + * Get the lower-case version of shortcut + * + * @return lower-case shortcut + */ + constexpr char shortcut_lower() const { + return shortcut >= 'A' && shortcut <= 'Z' ? + shortcut + ('a' - 'A') : shortcut; + } +}; + +/** + * Array of all flag information. + */ +constexpr std::array<MessageFlagInfo, 14> AllMessageFlagInfos = {{ + MessageFlagInfo{Flags::Draft, 'D', "draft", MessageFlagCategory::Mailfile, + "Draft (in progress)" + }, + MessageFlagInfo{Flags::Flagged, 'F', "flagged", MessageFlagCategory::Mailfile, + "User-flagged" + }, + MessageFlagInfo{Flags::Passed, 'P', "passed", MessageFlagCategory::Mailfile, + "Forwarded message" + }, + MessageFlagInfo{Flags::Replied, 'R', "replied", MessageFlagCategory::Mailfile, + "Replied-to" + }, + MessageFlagInfo{Flags::Seen, 'S', "seen", MessageFlagCategory::Mailfile, + "Viewed at least once" + }, + MessageFlagInfo{Flags::Trashed, 'T', "trashed", MessageFlagCategory::Mailfile, + "Marked for deletion" + }, + MessageFlagInfo{Flags::New, 'N', "new", MessageFlagCategory::Maildir, + "New message" + }, + MessageFlagInfo{Flags::Signed, 'z', "signed", MessageFlagCategory::Content, + "Cryptographically signed" + }, + MessageFlagInfo{Flags::Encrypted, 'x', "encrypted", MessageFlagCategory::Content, + "Encrypted" + }, + MessageFlagInfo{Flags::HasAttachment,'a', "attach", MessageFlagCategory::Content, + "Has at least one attachment" + }, + + MessageFlagInfo{Flags::Unread, 'u', "unread", MessageFlagCategory::Pseudo, + "New or not seen message" + }, + MessageFlagInfo{Flags::MailingList, 'l', "list", MessageFlagCategory::Content, + "Mailing list message" + }, + MessageFlagInfo{Flags::Personal, 'q', "personal", MessageFlagCategory::Content, + "Personal message" + }, + MessageFlagInfo{Flags::Calendar, 'c', "calendar", MessageFlagCategory::Content, + "Calendar invitation" + }, +}}; + + +/** + * Invoke some callable Func for each flag info + * + * @param func some callable + */ +template<typename Func> +constexpr void flag_infos_for_each(Func&& func) +{ + for (auto&& info: AllMessageFlagInfos) + func(info); +} + +/** + * Get flag info for some flag + * + * @param flag a singular flag + * + * @return the MessageFlagInfo, or Nothing in case of error. + */ +constexpr const Option<MessageFlagInfo> +flag_info(Flags flag) +{ + constexpr auto upper = static_cast<unsigned>(Flags::_final_); + const auto val = static_cast<unsigned>(flag); + + if (__builtin_popcount(val) != 1 || val >= upper) + return Nothing; + + return AllMessageFlagInfos[static_cast<unsigned>(__builtin_ctz(val))]; +} + +/** + * Get flag info for some flag + * + * @param shortcut shortcut character + * + * @return the MessageFlagInfo + */ +constexpr const Option<MessageFlagInfo> +flag_info(char shortcut) +{ + for (auto&& info : AllMessageFlagInfos) + if (info.shortcut == shortcut) + return info; + + return Nothing; +} + +/** + * Get flag info for some flag + * + * @param name of the message-flag. + * + * @return the MessageFlagInfo + */ +constexpr const Option<MessageFlagInfo> +flag_info(std::string_view name) +{ + for (auto&& info : AllMessageFlagInfos) + if (info.name == name) + return info; + + return Nothing; +} + +/** + * There are two string-based expression types for flags: + * 1) 'absolute': replace the existing flags + * 2_ 'delta' : flags as a delta of existing flags. + */ + +/** + * Get the (OR'ed) flags corresponding to an expression. + * + * @param expr the expression (a sequence of flag shortcut characters) + * @param ignore_invalid if @true, ignore invalid flags, otherwise return + * nullopt if an invalid flag is encountered + * + * @return the (OR'ed) flags or Flags::None + */ +constexpr Option<Flags> +flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false) +{ + Flags flags{Flags::None}; + + for (auto&& kar : expr) { + if (const auto& info{flag_info(kar)}; !info) { + if (!ignore_invalid) + return Nothing; + } else + flags |= info->flag; + } + + return flags; +} + +/** + * Calculate flags from existing flags and a delta expression + * + * Update @p flags with the flags in @p expr, where @p exprt consists of the the + * normal flag shortcut characters, prefixed with either '+' or '-', which means + * resp. "add this flag" or "remove this flag". + * + * So, e.g. "-N+S" would unset the NEW flag and set the SEEN flag, without + * affecting other flags. + * + * @param expr delta expression + * @param flags existing flags + * @param ignore_invalid if @true, ignore invalid flags, otherwise return + * nullopt if an invalid flag is encountered + * + * @return new flags, or nullopt in case of error + */ +constexpr Option<Flags> +flags_from_delta_expr(std::string_view expr, Flags flags, + bool ignore_invalid = false) +{ + if (expr.size() % 2 != 0) + return Nothing; + + for (auto u = 0U; u != expr.size(); u += 2) { + if (const auto& info{flag_info(expr[u + 1])}; !info) { + if (!ignore_invalid) + return Nothing; + } else { + switch (expr[u]) { + case '+': flags |= info->flag; break; + case '-': flags &= ~info->flag; break; + default: + if (!ignore_invalid) + return Nothing; + break; + } + } + } + + return flags; +} + +/** + * Calculate the flags from either 'absolute' or 'delta' expressions + * + * @param expr a flag expression, either 'delta' or 'absolute' + * @param flags optional: existing flags or none. Required for delta. + * + * @return either messages flags or Nothing in case of error. + */ +constexpr Option<Flags> +flags_from_expr(std::string_view expr, + Option<Flags> flags = Nothing) +{ + if (expr.empty()) + return Nothing; + + if (expr[0] == '+' || expr[0] == '-') + return flags_from_delta_expr( + expr, flags.value_or(Flags::None), true); + else + return flags_from_absolute_expr(expr, true); +} + +/** + * Filter out flags which are not in the given category + * + * @param flags flags + * @param cat category + * + * @return filter flags + */ +constexpr Flags +flags_filter(Flags flags, MessageFlagCategory cat) +{ + for (auto&& info : AllMessageFlagInfos) + if (info.category != cat) + flags &= ~info.flag; + return flags; +} + +/** + * Get a string representation of flags + * + * @param flags flags + * + * @return string as a sequence of message-flag shortcuts + */ +std::string to_string(Flags flags); + +} // namespace Mu + +#endif /* MU_FLAGS_HH__ */ diff --git a/lib/message/mu-message-file.cc b/lib/message/mu-message-file.cc new file mode 100644 index 0000000..3c86c24 --- /dev/null +++ b/lib/message/mu-message-file.cc @@ -0,0 +1,207 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb.bulk@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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-message-file.hh" + +using namespace Mu; + +Result<std::string> +Mu::maildir_from_path(const std::string& path, const std::string& root) +{ + const auto pos = path.find(root); + if (pos != 0 || path[root.length()] != '/') + return Err(Error{Error::Code::InvalidArgument, + "root '%s' is not a root for path '%s'", + root.c_str(), path.c_str()}); + + auto mdir{path.substr(root.length())}; + auto slash{mdir.rfind('/')}; + + if (G_UNLIKELY(slash == std::string::npos) || slash < 4) + return Err(Error{Error::Code::InvalidArgument, + "invalid path: %s", path.c_str()}); + mdir.erase(slash); + auto subdir = mdir.data() + slash - 4; + if (G_UNLIKELY(strncmp(subdir, "/cur", 4) != 0 && strncmp(subdir, "/new", 4))) + return Err(Error::Code::InvalidArgument, + "cannot find '/new' or '/cur' - invalid path: %s", + path.c_str()); + + if (mdir.length() == 4) + return "/"; + + mdir.erase(mdir.length() - 4); + + return Ok(std::move(mdir)); +} + +Mu::FileParts +Mu::message_file_parts(const std::string& file) +{ + const auto pos{file.find_last_of(":!;")}; + + /* no suffix at all? */ + if (pos == std::string::npos || + pos > file.length() - 3 || + file[pos + 1] != '2' || + file[pos + 2] != ',') + return FileParts{ file, ':', {}}; + + return FileParts { + file.substr(0, pos), + file[pos], + file.substr(pos + 3) + }; +} + +Mu::Result<DirFile> +Mu::base_message_dir_file(const std::string& path) +{ + constexpr auto newdir{ G_DIR_SEPARATOR_S "new"}; + + char *dirname{g_path_get_dirname(path.c_str())}; + bool is_new{!!g_str_has_suffix(dirname, newdir)}; + + std::string mdir{dirname, ::strlen(dirname) - 4}; + g_free(dirname); + + char *basename{g_path_get_basename(path.c_str())}; + std::string bname{basename}; + g_free(basename); + + return Ok(DirFile{std::move(mdir), std::move(bname), is_new}); +} + +Mu::Result<Mu::Flags> +Mu::flags_from_path(const std::string& path) +{ /* + * this gets us the source maildir filesystem path, the directory + * in which new/ & cur/ lives, and the source file + */ + auto dirfile{base_message_dir_file(path)}; + if (!dirfile) + return Err(std::move(dirfile.error())); + + /* a message under new/ is just.. New. Filename is not considered */ + if (dirfile->is_new) + return Ok(Flags::New); + + /* it's cur/ message, so parse the file name */ + const auto parts{message_file_parts(dirfile->file)}; + auto flags{flags_from_absolute_expr(parts.flags_suffix, + true/*ignore invalid*/)}; + if (!flags) { + /* LCOV_EXCL_START*/ + return Err(Error{Error::Code::InvalidArgument, + "invalid flags ('%s')", parts.flags_suffix.c_str()}); + /* LCOV_EXCL_STOP*/ + } + + /* of course, only _file_ flags are allowed */ + return Ok(flags_filter(flags.value(), MessageFlagCategory::Mailfile)); +} + + + + +#ifdef BUILD_TESTS + +#include "utils/mu-test-utils.hh" + +static void +test_maildir_from_path() +{ + std::array<std::tuple<std::string, std::string, std::string>, 1> test_cases = {{ + { "/home/foo/Maildir/hello/cur/msg123", "/home/foo/Maildir", "/hello" } + }}; + + for(auto&& tcase: test_cases) { + const auto res{maildir_from_path(std::get<0>(tcase), std::get<1>(tcase))}; + assert_valid_result(res); + assert_equal(*res, std::get<2>(tcase)); + } + + g_assert_false(!!maildir_from_path("/home/foo/Maildir/cur/test1", "/home/bar")); + g_assert_false(!!maildir_from_path("/x", "/x/y")); + g_assert_false(!!maildir_from_path("/home/a/Maildir/b/xxx/test", "/home/a/Maildir")); +} + +static void +test_base_message_dir_file() +{ + struct TestCase { + const std::string path; + DirFile expected; + }; + std::array<TestCase, 1> test_cases = {{ + { "/home/djcb/Maildir/foo/cur/msg:2,S", + { "/home/djcb/Maildir/foo", "msg:2,S", false } } + }}; + for(auto&& tcase: test_cases) { + const auto res{base_message_dir_file(tcase.path)}; + assert_valid_result(res); + assert_equal(res->dir, tcase.expected.dir); + assert_equal(res->file, tcase.expected.file); + g_assert_cmpuint(res->is_new, ==, tcase.expected.is_new); + } +} + +static void +test_flags_from_path() +{ + std::array<std::pair<std::string, Flags>, 5> test_cases = {{ + {"/home/foo/Maildir/test/cur/123456:2,FSR", + (Flags::Replied | Flags::Seen | Flags::Flagged)}, + {"/home/foo/Maildir/test/new/123456", Flags::New}, + {/* NOTE: when in new/, the :2,.. stuff is ignored */ + "/home/foo/Maildir/test/new/123456:2,FR", + Flags::New}, + {"/home/foo/Maildir/test/cur/123456:2,DTP", + (Flags::Draft | Flags::Trashed | Flags::Passed)}, + {"/home/foo/Maildir/test/cur/123456:2,S", Flags::Seen} + }}; + + for (auto&& tcase: test_cases) { + auto res{flags_from_path(tcase.first)}; + assert_valid_result(res); + /* LCOV_EXCL_START*/ + if (g_test_verbose()) { + g_print("%s -> <%s>\n", tcase.first.c_str(), + to_string(res.value()).c_str()); + g_assert_true(res.value() == tcase.second); + } + /*LCOV_EXCL_STOP*/ + } +} + + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_add_func("/message/file/maildir-from-path", + test_maildir_from_path); + g_test_add_func("/message/file/base-message-dir-file", + test_base_message_dir_file); + g_test_add_func("/message/file/flags-from-path", test_flags_from_path); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-message-file.hh b/lib/message/mu-message-file.hh new file mode 100644 index 0000000..09a9ed3 --- /dev/null +++ b/lib/message/mu-message-file.hh @@ -0,0 +1,98 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb.bulk@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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_MESSAGE_FILE_HH__ +#define MU_MESSAGE_FILE_HH__ + +#include "mu-flags.hh" +#include <utils/mu-result.hh> + +namespace Mu { + +/* + * The file-components, ie. + * 1631819685.fb7b279bbb0a7b66.evergrey:2,RS + * => { + * "1631819685.fb7b279bbb0a7b66.evergrey", + * ':', + * "2,", + * "RS" + * } + */ +struct FileParts { + std::string base; /**< basename */ + char separator; /**< separator */ + std::string flags_suffix; /**< suffix (with flags) */ +}; + +/** + * Get the file-parts for some message-file + * + * @param file path to some message file (does not have to exist) + * + * @return FileParts for the message file + */ +FileParts message_file_parts(const std::string& file); + + +struct DirFile { + std::string dir; + std::string file; + bool is_new; +}; + +/** + * Get information about the message file componemts + * + * @param path message path + * + * @return the components for the message file or an error. + */ +Result<DirFile> base_message_dir_file(const std::string& path); + + + +/** + * Get the Maildir flags from the full path of a mailfile. The flags are as + * specified in http://cr.yp.to/proto/maildir.html, plus Flag::New for new + * messages, ie the ones that live in new/. The flags are logically OR'ed. Note + * that the file does not have to exist; the flags are based on the path only. + * + * @param pathname of a mailfile; it does not have to refer to an + * actual message + * + * @return the message flags or an error + */ +Result<Flags> flags_from_path(const std::string& pathname); + +/** + * get the maildir for a certain message path, ie, the path *before* + * cur/ or new/ and *after* the root. + * + * @param path path for some message + * @param root filesystem root for the maildir + * + * @return the maildir or an Error + */ +Result<std::string> maildir_from_path(const std::string& path, + const std::string& root); +} // Mu + + +#endif /* MU_MESSAGE_FILE_HH__ */ diff --git a/lib/message/mu-message-part.cc b/lib/message/mu-message-part.cc new file mode 100644 index 0000000..8f0b763 --- /dev/null +++ b/lib/message/mu-message-part.cc @@ -0,0 +1,188 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#include "mu-message-part.hh" +#include "glibconfig.h" +#include "mu-mime-object.hh" +#include "utils/mu-utils.hh" +#include <string> + +using namespace Mu; + +MessagePart::MessagePart(const Mu::MimeObject& obj): + mime_obj{std::make_unique<Mu::MimeObject>(obj)} +{} + +MessagePart::MessagePart(const MessagePart& other): + MessagePart(*other.mime_obj) +{} + +MessagePart::~MessagePart() = default; + +const MimeObject& +MessagePart::mime_object() const noexcept +{ + return *mime_obj; +} + +Option<std::string> +MessagePart::cooked_filename() const noexcept +{ + // make a bit more pallatble. + auto cleanup = [](const std::string& name)->std::string { + std::string clean; + clean.reserve(name.length()); + for (auto& c: name) { + auto taboo{(::iscntrl(c) || c == G_DIR_SEPARATOR || + c == ' ' || c == '\\' || c == ':')}; + clean += (taboo ? '-' : c); + } + if (clean.size() > 1 && clean[0] == '-') + clean.erase(0, 1); + + return clean; + }; + + // a MimePart... use the name if there is one. + if (mime_object().is_part()) + return MimePart{mime_object()}.filename().map(cleanup); + + // MimeMessagepart. Construct a name based on subject. + if (mime_object().is_message_part()) { + auto msg{MimeMessagePart{mime_object()}.get_message()}; + if (!msg) + return Nothing; + else + return msg->subject() + .map(cleanup) + .value_or("no-subject") + ".eml"; + } + + return Nothing; +} + +Option<std::string> +MessagePart::raw_filename() const noexcept +{ + if (!mime_object().is_part()) + return Nothing; + else + return MimePart{mime_object()}.filename(); +} + + + +Option<std::string> +MessagePart::mime_type() const noexcept +{ + if (const auto ctype{mime_object().content_type()}; ctype) + return ctype->media_type() + "/" + ctype->media_subtype(); + else + return Nothing; +} + +Option<std::string> +MessagePart::content_description() const noexcept +{ + if (!mime_object().is_part()) + return Nothing; + else + return MimePart{mime_object()}.content_description(); +} + +size_t +MessagePart::size() const noexcept +{ + if (!mime_object().is_part()) + return 0; + else + return MimePart{mime_object()}.size(); +} + +bool +MessagePart::is_attachment() const noexcept +{ + if (!mime_object().is_part()) + return false; + else + return MimePart{mime_object()}.is_attachment(); +} + + +Option<std::string> +MessagePart::to_string() const noexcept +{ + if (mime_object().is_part()) + return MimePart{mime_object()}.to_string(); + else + return mime_object().to_string_opt(); +} + + + +Result<size_t> +MessagePart::to_file(const std::string& path, bool overwrite) const noexcept +{ + if (!mime_object().is_part()) + return Err(Error::Code::InvalidArgument, + "not a part"); + else + return MimePart{mime_object()}.to_file(path, overwrite); +} + +bool +MessagePart::is_signed() const noexcept +{ + return mime_object().is_multipart_signed(); +} + +bool +MessagePart::is_encrypted() const noexcept +{ + return mime_object().is_multipart_encrypted(); +} + +bool /* heuristic */ +MessagePart::looks_like_attachment() const noexcept +{ + auto matches=[](const MimeContentType& ctype, + const std::initializer_list<std::pair<const char*, const char*>>& ctypes) { + return std::find_if(ctypes.begin(), ctypes.end(), [&](auto&& item){ + return ctype.is_type(item.first, item.second); }) != ctypes.end(); + }; + + const auto ctype{mime_object().content_type()}; + if (!ctype) + return false; // no content-type: not an attachment. + + // we consider some parts _not_ to be attachments regardless of disposition + if (matches(*ctype,{{"application", "pgp-keys"}})) + return false; + + // we consider some parts to be attachments regardless of disposition + if (matches(*ctype,{{"image", "*"}, + {"audio", "*"}, + {"application", "*"}, + {"application", "x-patch"}})) + return true; + + // otherwise, rely on the disposition + return is_attachment(); +} diff --git a/lib/message/mu-message-part.hh b/lib/message/mu-message-part.hh new file mode 100644 index 0000000..b955fc8 --- /dev/null +++ b/lib/message/mu-message-part.hh @@ -0,0 +1,165 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#ifndef MU_MESSAGE_PART_HH__ +#define MU_MESSAGE_PART_HH__ + +#include <string> +#include <memory> +#include <utils/mu-option.hh> +#include <utils/mu-result.hh> + +namespace Mu { + +class MimeObject; // forward declaration; don't want to include for build-time + // reasons. + +class MessagePart { +public: + /** + * Construct MessagePart from a MimeObject + * + * @param obj + */ + MessagePart(const MimeObject& obj); + + /** + * Copy CTOR + * + * @param other + */ + MessagePart(const MessagePart& other); + + /** + * DTOR + * + */ + ~MessagePart(); + + + /** + * Get the underlying MimeObject; you need to include mu-mime-object.hh + * to do anything useful with it. + * + * @return reference to the mime-object + */ + const MimeObject& mime_object() const noexcept; + + /** + * Filename for the mime-part file. This is a "cooked" filename with + * unallowed characters removed. If there's no filename specified, + * construct one (such as in the case of MimeMessagePart). + * + * @see raw_filename() + * + * @return the name + */ + Option<std::string> cooked_filename() const noexcept; + + /** + * Name for the mime-part file, i.e., MimePart::filename + * + * @return the filename or Nothing if there is none + */ + Option<std::string> raw_filename() const noexcept; + + /** + * Mime-type for the mime-part (e.g. "text/plain") + * + * @return the mime-part or Nothing if there is none + */ + Option<std::string> mime_type() const noexcept; + + + /** + * Get the content description for this part, or Nothing + * + * @return the content description + */ + Option<std::string> content_description() const noexcept; + + /** + * Get the length of the (unencoded) MIME-part. + * + * @return the size + */ + size_t size() const noexcept; + + /** + * Does this part have an "attachment" disposition? Otherwise it is + * "inline". Note that does *not* map 1:1 to a message's HasAttachment + * flag (which uses looks_like_attachment()) + * + * @return true or false. + */ + bool is_attachment() const noexcept; + + + /** + * Does this part appear to be an attachment from an end-users point of + * view? This uses some heuristics to guess. Some parts for which + * is_attachment() is true may not "really" be attachments, and + * vice-versa + * + * @return true or false. + */ + bool looks_like_attachment() const noexcept; + + /** + * Is this part signed? + * + * @return true or false + */ + bool is_signed() const noexcept; + + + /** + * Is this part encrypted? + * + * @return true or false + */ + bool is_encrypted() const noexcept; + + + /** + * Write (decoded) mime-part contents to string + * + * @return a string or nothing if there is no contemt + */ + Option<std::string> to_string() const noexcept; + + /** + * Write (decoded) mime part to a file + * + * @param path path to file + * @param overwrite whether to possibly overwrite + * + * @return size of file or or an error. + */ + Result<size_t> to_file(const std::string& path, bool overwrite) const noexcept; + + struct Private; +private: + const std::unique_ptr<MimeObject> mime_obj; +}; + +} // namespace Mu + +#endif /* MU_MESSAGE_PART_HH__ */ diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc new file mode 100644 index 0000000..a8ff0f5 --- /dev/null +++ b/lib/message/mu-message.cc @@ -0,0 +1,838 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#include "mu-message.hh" +#include "gmime/gmime-references.h" +#include "gmime/gmime-stream-mem.h" +#include "mu-maildir.hh" + +#include <array> +#include <string> +#include <regex> +#include <utils/mu-util.h> +#include <utils/mu-utils.hh> +#include <utils/mu-error.hh> +#include <utils/mu-option.hh> + +#include <atomic> +#include <mutex> +#include <cstdlib> + +#include <glib.h> +#include <glib/gstdio.h> +#include <gmime/gmime.h> + +#include "gmime/gmime-message.h" +#include "mu-mime-object.hh" + +using namespace Mu; + +struct Message::Private { + Private(Message::Options options): opts{options} {} + Private(Message::Options options, Xapian::Document&& xdoc): + opts{options}, doc{std::move(xdoc)} {} + + Message::Options opts; + Document doc; + mutable Option<MimeMessage> mime_msg; + + Flags flags{}; + Option<std::string> mailing_list; + std::vector<Part> parts; + + ::time_t ctime{}; + + std::string cache_path; + /* + * we only need to index these, so we don't + * really need these copy if we re-arrange things + * a bit + */ + Option<std::string> body_txt; + Option<std::string> body_html; + Option<std::string> embedded; +}; + + +static void fill_document(Message::Private& priv); + +static Result<struct stat> +get_statbuf(const std::string& path) +{ + if (!g_path_is_absolute(path.c_str())) + return Err(Error::Code::File, "path '%s' is not absolute", + path.c_str()); + if (::access(path.c_str(), R_OK) != 0) + return Err(Error::Code::File, "file @ '%s' is not readable", + path.c_str()); + + struct stat statbuf{}; + if (::stat(path.c_str(), &statbuf) < 0) + return Err(Error::Code::File, "cannot stat %s: %s", path.c_str(), + g_strerror(errno)); + + if (!S_ISREG(statbuf.st_mode)) + return Err(Error::Code::File, "not a regular file: %s", path.c_str()); + + return Ok(std::move(statbuf)); +} + + +Message::Message(const std::string& path, Message::Options opts): + priv_{std::make_unique<Private>(opts)} +{ + const auto statbuf{get_statbuf(path)}; + if (!statbuf) + throw statbuf.error(); + + priv_->ctime = statbuf->st_ctime; + + init_gmime(); + if (auto msg{MimeMessage::make_from_file(path)}; !msg) + throw msg.error(); + else + priv_->mime_msg = std::move(msg.value()); + + auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), NULL))}; + if (xpath) + priv_->doc.add(Field::Id::Path, std::move(xpath.value())); + + priv_->doc.add(Field::Id::Size, static_cast<int64_t>(statbuf->st_size)); + + // rest of the fields + fill_document(*priv_); +} + +Message::Message(const std::string& text, const std::string& path, + Message::Options opts): + priv_{std::make_unique<Private>(opts)} +{ + if (text.empty()) + throw Error{Error::Code::InvalidArgument, "text must not be empty"}; + + if (!path.empty()) { + auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), {}))}; + if (xpath) + priv_->doc.add(Field::Id::Path, std::move(xpath.value())); + } + + priv_->ctime = ::time({}); + + priv_->doc.add(Field::Id::Size, static_cast<int64_t>(text.size())); + + init_gmime(); + if (auto msg{MimeMessage::make_from_text(text)}; !msg) + throw msg.error(); + else + priv_->mime_msg = std::move(msg.value()); + + fill_document(*priv_); +} + + +Message::Message(Message&& other) noexcept +{ + *this = std::move(other); +} + +Message& +Message::operator=(Message&& other) noexcept +{ + if (this != &other) + priv_ = std::move(other.priv_); + + return *this; +} + +Message::Message(Xapian::Document&& doc): + priv_{std::make_unique<Private>(Message::Options::None, std::move(doc))} +{} + + +Message::~Message() = default; + +const Mu::Document& +Message::document() const +{ + return priv_->doc; +} + + +unsigned +Message::docid() const +{ + return priv_->doc.xapian_document().get_docid(); +} + + +const Mu::Sexp::List& +Message::to_sexp_list() const +{ + return priv_->doc.sexp_list(); +} + +void +Message::update_cached_sexp() +{ + priv_->doc.update_cached_sexp(); +} + +Result<void> +Message::set_maildir(const std::string& maildir) +{ + /* sanity check a little bit */ + + if (maildir.empty() || + maildir.at(0) != '/' || + (maildir.size() > 1 && maildir.at(maildir.length()-1) == '/')) + return Err(Error::Code::Message, + "'%s' is not a valid maildir", maildir.c_str()); + + const auto path{document().string_value(Field::Id::Path)}; + if (path == maildir || path.find(maildir) == std::string::npos) + return Err(Error::Code::Message, + "'%s' is not a valid maildir for message @ %s", + maildir.c_str(), path.c_str()); + + priv_->doc.remove(Field::Id::Maildir); + priv_->doc.add(Field::Id::Maildir, maildir); + + return Ok(); +} + +void +Message::set_flags(Flags flags) +{ + priv_->doc.remove(Field::Id::Flags); + priv_->doc.add(flags); +} + +bool +Message::load_mime_message(bool reload) const +{ + if (priv_->mime_msg && !reload) + return true; + + const auto path{document().string_value(Field::Id::Path)}; + if (auto mime_msg{MimeMessage::make_from_file(path)}; !mime_msg) { + g_warning("failed to load '%s': %s", + path.c_str(), mime_msg.error().what()); + return false; + } else { + priv_->mime_msg = std::move(mime_msg.value()); + fill_document(*priv_); + return true; + } +} + +void +Message::unload_mime_message() const +{ + priv_->mime_msg = Nothing; +} + +bool +Message::has_mime_message() const +{ + return !!priv_->mime_msg; +} + + +static Priority +get_priority(const MimeMessage& mime_msg) +{ + constexpr std::array<std::pair<std::string_view, Priority>, 10> + prio_alist = {{ + {"high", Priority::High}, + {"1", Priority::High}, + {"2", Priority::High}, + + {"normal", Priority::Normal}, + {"3", Priority::Normal}, + + {"low", Priority::Low}, + {"list", Priority::Low}, + {"bulk", Priority::Low}, + {"4", Priority::Low}, + {"5", Priority::Low} + }}; + + const auto opt_str = mime_msg.header("Precedence") + .disjunction(mime_msg.header("X-Priority")) + .disjunction(mime_msg.header("Importance")); + + if (!opt_str) + return Priority::Normal; + + const auto it = seq_find_if(prio_alist, [&](auto&& item) { + return g_ascii_strncasecmp(item.first.data(), opt_str->c_str(), + item.first.size()) == 0; }); + + return it == prio_alist.cend() ? Priority::Normal : it->second; +} + + +/* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */ +static std::vector<std::string> +extract_tags(const MimeMessage& mime_msg) +{ + constexpr std::array<std::pair<const char*, char>, 3> tag_headers = {{ + {"X-Label", ' '}, {"X-Keywords", ','}, {"Keywords", ' '} + }}; + static const auto strip_rx{std::regex("^\\s+| +$|( )\\s+")}; + + std::vector<std::string> tags; + seq_for_each(tag_headers, [&](auto&& item) { + if (auto&& hdr = mime_msg.header(item.first); hdr) { + for (auto&& tagval : split(*hdr, item.second)) { + tags.emplace_back( + std::regex_replace(tagval, strip_rx, "$1")); + } + } + }); + + return tags; +} + +static Option<std::string> +get_mailing_list(const MimeMessage& mime_msg) +{ + char *dechdr, *res; + const char *b, *e; + + const auto hdr{mime_msg.header("List-Id")}; + if (!hdr) + return {}; + + dechdr = g_mime_utils_header_decode_phrase(NULL, hdr->c_str()); + if (!dechdr) + return {}; + + e = NULL; + b = ::strchr(dechdr, '<'); + if (b) + e = strchr(b, '>'); + + if (b && e) + res = g_strndup(b + 1, e - b - 1); + else + res = g_strdup(dechdr); + + g_free(dechdr); + + return to_string_opt_gchar(std::move(res)); +} + +static void +append_text(Option<std::string>& str, Option<std::string> app) +{ + if (!str) + str = app; + else if (app) + str.value() += app.value(); +} + +static void +accumulate_text(const MimePart& part, Message::Private& info, + const MimeContentType& ctype) +{ + if (!ctype.is_type("text", "*")) + return; /* not a text type */ + + if (part.is_attachment()) + append_text(info.embedded, part.to_string()); + else if (ctype.is_type("text", "plain")) + append_text(info.body_txt, part.to_string()); + else if (ctype.is_type("text", "html")) + append_text(info.body_html, part.to_string()); +} + + +static bool /* heuristic */ +looks_like_attachment(const MimeObject& parent, const MessagePart& mpart) +{ + if (parent) { /* crypto multipart children are not considered attachments */ + if (const auto parent_ctype{parent.content_type()}; parent_ctype) { + if (parent_ctype->is_type("multipart", "signed") || + parent_ctype->is_type("multipart", "encrypted")) + return false; + } + } + + return mpart.looks_like_attachment(); +} + + +static void +process_part(const MimeObject& parent, const MimePart& part, + Message::Private& info, const MessagePart& mpart) +{ + const auto ctype{part.content_type()}; + if (!ctype) + return; + + // flag as calendar, if not already + if (none_of(info.flags & Flags::Calendar) && + ctype->is_type("text", "calendar")) + info.flags |= Flags::Calendar; + + // flag as attachment, if not already. + if (none_of(info.flags & Flags::HasAttachment) && + looks_like_attachment(parent, mpart)) + info.flags |= Flags::HasAttachment; + + // if there are text parts, gather. + accumulate_text(part, info, *ctype); +} + + +static void +process_message_part(const MimeMessagePart& msg_part, + Message::Private& info) +{ + auto submsg{msg_part.get_message()}; + if (!submsg) + return; + + submsg->for_each([&](auto&& parent, auto&& child_obj) { + + /* XXX: we only handle one level */ + + if (!child_obj.is_part()) + return; + + const auto ctype{child_obj.content_type()}; + if (!ctype || !ctype->is_type("text", "*")) + return; + + append_text(info.embedded, MimePart{child_obj}.to_string()); + }); +} + +static void +handle_object(const MimeObject& parent, + const MimeObject& obj, Message::Private& info); + + +static void +handle_encrypted(const MimeMultipartEncrypted& part, Message::Private& info) +{ + if (!any_of(info.opts & Message::Options::Decrypt)) { + /* just added to the list */ + info.parts.emplace_back(part); + return; + } + + const auto proto{part.content_type_parameter("protocol").value_or("unknown")}; + const auto ctx = MimeCryptoContext::make(proto); + if (!ctx) { + g_warning("failed to create context for protocol <%s>", + proto.c_str()); + return; + } + + auto res{part.decrypt(*ctx)}; + if (!res) { + g_warning("failed to decrypt: %s", res.error().what()); + return; + } + + if (res->first.is_multipart()) { + MimeMultipart{res->first}.for_each( + [&](auto&& parent, auto&& child_obj) { + handle_object(parent, child_obj, info); + }); + + } else + handle_object(part, res->first, info); +} + + +static void +handle_object(const MimeObject& parent, + const MimeObject& obj, Message::Private& info) +{ + /* if it's an encrypted part we should decrypt, recurse */ + if (obj.is_multipart_encrypted()) + handle_encrypted(MimeMultipartEncrypted{obj}, info); + else if (obj.is_part() || + obj.is_message_part() || + obj.is_multipart_signed() || + obj.is_multipart_encrypted()) + info.parts.emplace_back(obj); + + if (obj.is_part()) + process_part(parent, obj, info, info.parts.back()); + else if (obj.is_message_part()) + process_message_part(obj, info); + else if (obj.is_multipart_signed()) + info.flags |= Flags::Signed; + else if (obj.is_multipart_encrypted()) { + /* FIXME: An encrypted part might be signed at the same time. + * In that case the signed flag is lost. */ + info.flags |= Flags::Encrypted; + } else if (obj.is_mime_application_pkcs7_mime()) { + MimeApplicationPkcs7Mime smime(obj); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + // CompressedData, CertsOnly, Unknown + switch (smime.smime_type()) { + case Mu::MimeApplicationPkcs7Mime::SecureMimeType::SignedData: + info.flags |= Flags::Signed; + break; + case Mu::MimeApplicationPkcs7Mime::SecureMimeType::EnvelopedData: + info.flags |= Flags::Encrypted; + break; + default: + break; + } +#pragma GCC diagnostic pop + } +} + +/** + * This message -- recursively walk through message, and initialize some + * other values that depend on another. + * + * @param mime_msg + * @param path + * @param info + */ +static void +process_message(const MimeMessage& mime_msg, const std::string& path, + Message::Private& info) +{ + /* only have file-flags when there's a path. */ + if (!path.empty()) { + info.flags = flags_from_path(path).value_or(Flags::None); + /* pseudo-flag --> unread means either NEW or NOT SEEN, just + * for searching convenience */ + if (any_of(info.flags & Flags::New) || none_of(info.flags & Flags::Seen)) + info.flags |= Flags::Unread; + } + + // parts + mime_msg.for_each([&](auto&& parent, auto&& child_obj) { + handle_object(parent, child_obj, info); + }); + + // get the mailing here, and use it do update flags, too. + info.mailing_list = get_mailing_list(mime_msg); + if (info.mailing_list) + info.flags |= Flags::MailingList; +} + +static Mu::Result<std::string> +calculate_sha256(const std::string& path) +{ + g_autoptr(GChecksum) checksum{g_checksum_new(G_CHECKSUM_SHA256)}; + + FILE *file{::fopen(path.c_str(), "r")}; + if (!file) + return Err(Error{Error::Code::File, "failed to open %s: %s", + path.c_str(), ::strerror(errno)}); + + std::array<uint8_t, 4096> buf{}; + while (true) { + const auto n = ::fread(buf.data(), 1, buf.size(), file); + if (n == 0) + break; + g_checksum_update(checksum, buf.data(), n); + } + + bool has_err = ::ferror(file) != 0; + ::fclose(file); + + if (has_err) + return Err(Error{Error::Code::File, "failed to read %s", path.c_str()}); + + return Ok(g_checksum_get_string(checksum)); +} + +/** + * Get a fake-message-id for a message without one. + * + * @param path message path + * + * @return a fake message-id + */ +static std::string +fake_message_id(const std::string& path) +{ + constexpr auto mu_suffix{"@mu.id"}; + + // not a very good message-id, only for testing. + if (path.empty() || ::access(path.c_str(), R_OK) != 0) + return format("%08x%s", g_str_hash(path.c_str()), mu_suffix); + if (const auto sha256_res{calculate_sha256(path)}; !sha256_res) + return format("%08x%s", g_str_hash(path.c_str()), mu_suffix); + else + return format("%s%s", sha256_res.value().c_str(), mu_suffix); +} + +/* many of the doc.add(fiels ....) automatically update the sexp-list as well; + * however, there are some _extra_ values in the sexp-list that are not + * based on a field. So we add them here. + */ + + + +static void +doc_add_list_post(Document& doc, const MimeMessage& mime_msg) +{ + /* some mailing lists do not set the reply-to; see pull #1278. So for + * those cases, check the List-Post address and use that instead */ + + GMatchInfo* minfo; + GRegex* rx; + const auto list_post{mime_msg.header("List-Post")}; + if (!list_post) + return; + + rx = g_regex_new("<?mailto:([a-z0-9!@#$%&'*+-/=?^_`{|}~]+)>?", + G_REGEX_CASELESS, (GRegexMatchFlags)0, {}); + g_return_if_fail(rx); + + Contacts contacts; + if (g_regex_match(rx, list_post->c_str(), (GRegexMatchFlags)0, &minfo)) { + auto address = (char*)g_match_info_fetch(minfo, 1); + contacts.push_back(Contact(address)); + g_free(address); + } + + g_match_info_free(minfo); + g_regex_unref(rx); + + doc.add_extra_contacts(":list-post", contacts); +} + +static void +doc_add_reply_to(Document& doc, const MimeMessage& mime_msg) +{ + doc.add_extra_contacts(":reply-to", mime_msg.contacts(Contact::Type::ReplyTo)); +} + +static void +fill_document(Message::Private& priv) +{ + /* hunt & gather info from message tree */ + Document& doc{priv.doc}; + MimeMessage& mime_msg{priv.mime_msg.value()}; + + const auto path{doc.string_value(Field::Id::Path)}; + const auto refs{mime_msg.references()}; + const auto& raw_message_id = mime_msg.message_id(); + const auto message_id = raw_message_id.has_value() && !raw_message_id->empty() + ? *raw_message_id + : fake_message_id(path); + + process_message(mime_msg, path, priv); + + doc_add_list_post(doc, mime_msg); /* only in sexp */ + doc_add_reply_to(doc, mime_msg); /* only in sexp */ + + field_for_each([&](auto&& field) { + /* insist on expliclity handling each */ +#pragma GCC diagnostic push +#pragma GCC diagnostic error "-Wswitch" + switch(field.id) { + case Field::Id::Bcc: + doc.add(field.id, mime_msg.contacts(Contact::Type::Bcc)); + break; + case Field::Id::BodyText: + doc.add(field.id, priv.body_txt); + + break; + case Field::Id::Cc: + doc.add(field.id, mime_msg.contacts(Contact::Type::Cc)); + break; + case Field::Id::Changed: + doc.add(field.id, priv.ctime); + break; + case Field::Id::Date: + doc.add(field.id, mime_msg.date()); + break; + case Field::Id::EmbeddedText: + doc.add(field.id, priv.embedded); + break; + case Field::Id::File: + for (auto&& part: priv.parts) + doc.add(field.id, part.raw_filename()); + break; + case Field::Id::Flags: + doc.add(priv.flags); + break; + case Field::Id::From: + doc.add(field.id, mime_msg.contacts(Contact::Type::From)); + break; + case Field::Id::Maildir: /* already */ + break; + case Field::Id::MailingList: + doc.add(field.id, priv.mailing_list); + break; + case Field::Id::MessageId: + doc.add(field.id, message_id); + break; + case Field::Id::MimeType: + for (auto&& part: priv.parts) + doc.add(field.id, part.mime_type()); + break; + case Field::Id::Path: /* already */ + break; + case Field::Id::Priority: + doc.add(get_priority(mime_msg)); + break; + case Field::Id::References: + if (!refs.empty()) + doc.add(field.id, refs); + break; + case Field::Id::Size: /* already */ + break; + case Field::Id::Subject: + doc.add(field.id, mime_msg.subject()); + break; + case Field::Id::Tags: + if (auto&& tags{extract_tags(mime_msg)}; !tags.empty()) + doc.add(field.id, tags); + break; + case Field::Id::ThreadId: + // either the oldest reference, or otherwise the message id + doc.add(field.id, refs.empty() ? message_id : refs.at(0)); + break; + case Field::Id::To: + doc.add(field.id, mime_msg.contacts(Contact::Type::To)); + break; + /* internal fields */ + case Field::Id::XBodyHtml: + doc.add(field.id, priv.body_html); + break; + /* LCOV_EXCL_START */ + case Field::Id::_count_: + default: + break; + /* LCOV_EXCL_STOP */ + } +#pragma GCC diagnostic pop + + }); +} + +Option<std::string> +Message::header(const std::string& header_field) const +{ + load_mime_message(); + return priv_->mime_msg->header(header_field); +} + +Option<std::string> +Message::body_text() const +{ + load_mime_message(); + return priv_->body_txt; +} + +Option<std::string> +Message::body_html() const +{ + load_mime_message(); + return priv_->body_html; +} + +Contacts +Message::all_contacts() const +{ + Contacts contacts; + + if (!load_mime_message()) + return contacts; /* empty */ + + return priv_->mime_msg->contacts(Contact::Type::None); /* get all types */ +} + +const std::vector<Message::Part>& +Message::parts() const +{ + if (!load_mime_message()) { + static std::vector<Message::Part> empty; + return empty; + } + + return priv_->parts; +} + +Result<std::string> +Message::cache_path(Option<size_t> index) const +{ + /* create tmpdir for this message, if needed */ + if (priv_->cache_path.empty()) { + GError *err{}; + auto tpath{to_string_opt_gchar(g_dir_make_tmp("mu-cache-XXXXXX", &err))}; + if (!tpath) + return Err(Error::Code::File, &err, "failed to create temp dir"); + + priv_->cache_path = std::move(tpath.value()); + } + + if (index) { + GError *err{}; + auto tpath = format("%s/%zu", priv_->cache_path.c_str(), *index); + if (g_mkdir(tpath.c_str(), 0700) != 0) + return Err(Error::Code::File, &err, + "failed to create cache dir '%s'; err=%d", + tpath.c_str(), errno); + return Ok(std::move(tpath)); + } else + + return Ok(std::string{priv_->cache_path}); +} + +// for now this only remove stray '/' at the end +std::string +Message::sanitize_maildir(const std::string& mdir) +{ + if (mdir.size() > 1 && mdir.at(mdir.length()-1) == '/') + return mdir.substr(0, mdir.length() - 1); + else + return mdir; +} + +Result<void> +Message::update_after_move(const std::string& new_path, + const std::string& new_maildir, + Flags new_flags) +{ + if (auto statbuf{get_statbuf(new_path)}; !statbuf) + return Err(statbuf.error()); + else + priv_->ctime = statbuf->st_ctime; + + priv_->doc.remove(Field::Id::Path); + priv_->doc.remove(Field::Id::Changed); + + priv_->doc.add(Field::Id::Path, new_path); + priv_->doc.add(Field::Id::Changed, priv_->ctime); + + set_flags(new_flags); + + if (const auto res = set_maildir(sanitize_maildir(new_maildir)); !res) + return res; + + return Ok(); +} diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh new file mode 100644 index 0000000..3f2e001 --- /dev/null +++ b/lib/message/mu-message.hh @@ -0,0 +1,481 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_MESSAGE_HH__ +#define MU_MESSAGE_HH__ + +#include <memory> +#include <string> +#include <vector> +#include "mu-contact.hh" +#include "mu-priority.hh" +#include "mu-flags.hh" +#include "mu-fields.hh" +#include "mu-document.hh" +#include "mu-message-part.hh" +#include "mu-message-file.hh" + +#include <xapian.h> + +#include "utils/mu-utils.hh" +#include "utils/mu-option.hh" +#include "utils/mu-result.hh" +#include "utils/mu-sexp.hh" + +namespace Mu { + +class Message { +public: + enum struct Options { + None = 0, /**< Defaults */ + Decrypt = 1 << 0, /**< Attempt to decrypt */ + RetrieveKeys = 1 << 1, /**< Auto-retrieve crypto keys (implies network + * access) */ + }; + + /** + * Move CTOR + * + * @param some other message + */ + Message(Message&& other) noexcept; + + + /** + * operator= + * + * @param other move some object object + * + * @return + */ + Message& operator=(Message&& other) noexcept; + + /** + * Construct a message based on a path + * + * @param path path to message + * @param opts options + * + * @return a message or an error + */ + static Result<Message> make_from_path(const std::string& path, + Options opts={}) try { + return Ok(Message{path,opts}); + } catch (Error& err) { + return Err(err); + } + /* LCOV_EXCL_START */ + catch (...) { + return Err(Mu::Error(Error::Code::Message, + "failed to create message from path")); + } + /* LCOV_EXCL_STOP */ + + /** + * Construct a message based on a Xapian::Document + * + * @param doc a Mu Document + * + * @return a message or an error + */ + static Result<Message> make_from_document(Xapian::Document&& doc) try { + return Ok(Message{std::move(doc)}); + } catch (Error& err) { + return Err(err); + } + /* LCOV_EXCL_START */ + catch (...) { + return Err(Mu::Error(Error::Code::Message, + "failed to create message from document")); + } + /* LCOV_EXCL_STOP */ + + + /** + * Construct a message from a string. This is mostly useful for testing. + * + * @param text message text + * @param path path to message - optional; path does not have to exist. + * @param opts options + * + * @return a message or an error + */ + static Result<Message> make_from_text(const std::string& text, + const std::string& path={}, + Options opts={}) try { + return Ok(Message{text, path, opts}); + } catch (Error& err) { + return Err(err); + } + /* LCOV_EXCL_START */ + catch (...) { + return Err(Mu::Error(Error::Code::Message, + "failed to create message from text")); + } + /* LCOV_EXCL_STOP */ + + /** + * DTOR + */ + ~Message(); + + /** + * Get the document. + * + * + * @return document + */ + const Document& document() const; + + + /** + * Get the document-id, or 0 if non-existent. + * + * @return document id + */ + unsigned docid() const; + + /** + * Get the file system path of this message + * + * @return the path of this Message or NULL in case of error. + * the returned string should *not* be modified or freed. + */ + std::string path() const { return document().string_value(Field::Id::Path); } + + /** + * Get the sender (From:) of this message + * + * @return the sender(s) of this Message + */ + Contacts from() const { return document().contacts_value(Field::Id::From); } + + /** + * Get the recipient(s) (To:) for this message + * + * @return recipients + */ + Contacts to() const { return document().contacts_value(Field::Id::To); } + + /** + * Get the recipient(s) (Cc:) for this message + * + * @return recipients + */ + Contacts cc() const { return document().contacts_value(Field::Id::Cc); } + + /** + * Get the recipient(s) (Bcc:) for this message + * + * @return recipients + */ + Contacts bcc() const { return document().contacts_value(Field::Id::Bcc); } + + /** + * Get the maildir this message resides in; i.e., if the path is + * ~/Maildir/foo/bar/cur/msg, the maildir would typically be foo/bar + * + * This is determined when _storing_ the message (which uses + * set_maildir()) + * + * @return the maildir requested or empty */ + std::string maildir() const { return document().string_value(Field::Id::Maildir); } + + /** + * Set the maildir for this message. This is for use by the _store_ when + * it has determined the maildir for this message from the message's path and + * the root-maildir known by the store. + * + * @param maildir the maildir for this message + * + * @return Ok() or some error if the maildir is invalid + */ + Result<void> set_maildir(const std::string& maildir); + + /** + * Clean up the maildir. This is for internal use, but exposed for testing. + * For now cleaned-up means "stray trailing / removed". + * + * @param maildir some maildir + * + * @return a cleaned-up version + */ + static std::string sanitize_maildir(const std::string& maildir); + + /** + * Get the subject of this message + * + * @return the subject of this Message + */ + std::string subject() const { return document().string_value(Field::Id::Subject); } + + /** + * Get the Message-Id of this message + * + * @return the Message-Id of this message (without the enclosing <>), or + * a fake message-id for messages that don't have them. + * + * For file-backed message, this fake message-id is based on a hash of the + * message contents. For non-file-backed (test) messages, some other value + * is concocted. + */ + std::string message_id() const { return document().string_value(Field::Id::MessageId);} + + /** + * get the mailing list for a message, i.e. the mailing-list + * identifier in the List-Id header. + * + * @return the mailing list id for this message (without the enclosing <>) + * or NULL in case of error or if there is none. + */ + std::string mailing_list() const { return document().string_value(Field::Id::MailingList);} + + /** + * get the message date/time (the Date: field) as time_t + * + * @return message date/time or 0 in case of error or if there + * is no such header. + */ + ::time_t date() const { + return static_cast<::time_t>(document().integer_value(Field::Id::Date)); + } + + /** + * get the last change-time this message. For path/document-based + * messages this corresponds with the ctime of the underlying file; for + * the text-based ones (as used for testing) it is the creation time. + * + * @return last-change time or 0 if unknown + */ + ::time_t changed() const { + return static_cast<::time_t>(document().integer_value(Field::Id::Changed)); + } + + /** + * get the flags for this message. + * + * @return the file/content flags + */ + Flags flags() const { return document().flags_value(); } + + + /** + * Update the flags for this message. This is useful for flags + * that can only be determined after the message has been created already, + * such as the 'personal' flag. + * + * @param flags new flags. + */ + void set_flags(Flags flags); + + /** + * get the message priority for this message. The X-Priority, X-MSMailPriority, + * Importance and Precedence header are checked, in that order. if no known or + * explicit priority is set, Priority::Id::Normal is assumed + * + * @return the message priority + */ + Priority priority() const { return document().priority_value(); } + + /** + * get the file size in bytes of this message + * + * @return the filesize + */ + size_t size() const { return static_cast<size_t>(document().integer_value(Field::Id::Size)); } + + /** + * Get the (possibly empty) list of references (consisting of both the + * References and In-Reply-To fields), with the oldest first and the + * direct parent as the last one. Note, any reference (message-id) will + * appear at most once, duplicates and fake-message-id (see impls) are + * filtered out. + * + * @return a vec with the references for this msg. + */ + std::vector<std::string> references() const { + return document().string_vec_value(Field::Id::References); + } + + /** + * Get the thread-id for this message. This is the message-id of the + * oldest-known (grand) parent, or the message-id of this message if + * none. + * + * @return the thread id. + */ + std::string thread_id() const { + return document().string_value(Field::Id::ThreadId); + } + + /** + * get the list of tags (ie., X-Label) + * + * @param msg a valid MuMsg + * + * @return a list with the tags for this msg. Don't modify/free + */ + std::vector<std::string> tags() const { + return document() + .string_vec_value(Field::Id::Tags); + } + + /** + * Get the cached s-expression for this message, or {} if not available. + * + * @return sexp or empty. + */ + std::string cached_sexp() const { + return document().cached_sexp(); + } + + /* + * Convert to Sexp + */ + + /** + * Get the s-expression for this message. Stays valid as long + * as this message is. + * + * @return a Mu::Sexp::List representing the message. + */ + const Mu::Sexp::List& to_sexp_list() const; + Mu::Sexp to_sexp() const { + return Sexp::make_list(Sexp::List(to_sexp_list())); + } + + /** + * Update the cached sexp for this message which is stored in the + * document. This should be done immediately before storing it in the + * database. + * + */ + void update_cached_sexp(); + + /* + * And some non-const message, for updating an existing + * message after a file-system move. + * + * @return Ok or an error. + */ + Result<void> update_after_move(const std::string& new_path, + const std::string& new_maildir, + Flags new_flags); + /* + * Below require a file-backed message, which is a relatively slow + * if there isn't one already; see load_mime_message() + */ + + /** + * Get the text body + * + * @return text body + */ + Option<std::string> body_text() const; + + /** + * Get the HTML body + * + * @return text body + */ + Option<std::string> body_html() const; + + /** + * Get some message-header + * + * @param header_field name of the header + * + * @return the value (UTF-8), or Nothing. + */ + Option<std::string> header(const std::string& header_field) const; + + + /** + * Get all contacts for this message. + * + * @return contacts + */ + Contacts all_contacts() const; + + /** + * Get information about MIME-parts in this message. + * + * @return mime-part info. + */ + using Part = MessagePart; + const std::vector<Part>& parts() const; + + /** + * Get the path to a cche directory for this message, which + * is useful for temporarily saving attachments + * + * @param index optionally, create <cache-path>/<index> instead; + * this is useful for having part-specific subdirectories. + * + * @return path to a (created) cache directory, or an error. + */ + Result<std::string> cache_path(Option<size_t> index={}) const; + + + /** + * Load the GMime (file) message (for a database-backed message), + * if not already (but see @param reload). + * + * Affects cached-state only, so we still mark this as 'const' + * + * @param reload whether to force reloading (even if already) + * + * @return true if loading worked; false otherwise. + */ + bool load_mime_message(bool reload=false) const; + + /** + * Clear the GMime message. + * + * Affects cached-state only, so we still mark this as 'const' + */ + void unload_mime_message() const; + + /** + * Has a (file-base) GMime message been loaded? + * + * + * @return true or false + */ + bool has_mime_message() const; + + struct Private; + + /* + * Usually the make_ builders are better to create a message, but in + * some special cases, we need a heap-allocated message... */ + + Message(Xapian::Document&& xdoc); + Message(const std::string& path, Options opts); + +private: + Message(const std::string& str, const std::string& path, Options opt); + + std::unique_ptr<Private> priv_; + +}; // Message +MU_ENABLE_BITOPS(Message::Options); + +} // Mu +#endif /* MU_MESSAGE_HH__ */ diff --git a/lib/message/mu-mime-object.cc b/lib/message/mu-mime-object.cc new file mode 100644 index 0000000..17b56f0 --- /dev/null +++ b/lib/message/mu-mime-object.cc @@ -0,0 +1,790 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#include "mu-mime-object.hh" +#include "gmime/gmime-message.h" +#include "utils/mu-utils.hh" +#include <mutex> +#include <regex> +#include <fcntl.h> +#include <errno.h> + +using namespace Mu; + + + +/* note, we do the gmime initialization here rather than in mu-runtime, because this way + * we don't need mu-runtime for simple cases -- such as our unit tests. Also note that we + * need gmime init even for the doc backend, as we use the address parsing functions also + * there. */ + +void +Mu::init_gmime(void) +{ + // fast path. + static bool gmime_initialized = false; + if (gmime_initialized) + return; + + static std::mutex gmime_lock; + std::lock_guard lock (gmime_lock); + if (gmime_initialized) + return; // already + + g_debug("initializing gmime %u.%u.%u", + gmime_major_version, + gmime_minor_version, + gmime_micro_version); + + g_mime_init(); + gmime_initialized = true; + + std::atexit([] { + g_debug("shutting down gmime"); + g_mime_shutdown(); + gmime_initialized = false; + }); +} + + +std::string +Mu::address_rfc2047(const Contact& contact) +{ + init_gmime(); + + InternetAddress *addr = + internet_address_mailbox_new(contact.name.c_str(), + contact.email.c_str()); + + std::string encoded = to_string_gchar( + internet_address_to_string(addr, {}, true)); + + g_object_unref(addr); + + return encoded; +} + + +/* + * MimeObject + */ + +Option<std::string> +MimeObject::header(const std::string& hdr) const noexcept +{ + if (auto val{g_mime_object_get_header(self(), hdr.c_str())}; !val) + return Nothing; + else if (!g_utf8_validate(val, -1, {})) + return utf8_clean(val); + else + return std::string{val}; +} + + +std::vector<std::pair<std::string, std::string>> +MimeObject::headers() const noexcept +{ + GMimeHeaderList *lst; + + lst = g_mime_object_get_header_list(self()); /* _not_ owned */ + if (!lst) + return {}; + + std::vector<std::pair<std::string, std::string>> hdrs; + const auto hdr_num{g_mime_header_list_get_count(lst)}; + + for (int i = 0; i != hdr_num; ++i) { + GMimeHeader *hdr{g_mime_header_list_get_header_at(lst, i)}; + if (!hdr) /* ^^^ _not_ owned */ + continue; + const auto name{g_mime_header_get_name(hdr)}; + const auto val{g_mime_header_get_value(hdr)}; + if (!name || !val) + continue; + hdrs.emplace_back(name, val); + } + + return hdrs; +} + + + +Result<size_t> +MimeObject::write_to_stream(const MimeFormatOptions& f_opts, + MimeStream& stream) const +{ + auto written = g_mime_object_write_to_stream(self(), f_opts.get(), + GMIME_STREAM(stream.object())); + if (written < 0) + return Err(Error::Code::File, "failed to write mime-object to stream"); + else + return Ok(static_cast<size_t>(written)); +} + +Option<std::string> +MimeObject::to_string_opt() const noexcept +{ + auto stream{MimeStream::make_mem()}; + if (!stream) { + g_warning("failed to create mem stream"); + return Nothing; + } + + const auto written = g_mime_object_write_to_stream( + self(), {}, GMIME_STREAM(stream.object())); + if (written < 0) { + g_warning("failed to write object to stream"); + return Nothing; + } + + std::string buffer; + buffer.resize(written + 1); + stream.reset(); + + auto bytes{g_mime_stream_read(GMIME_STREAM(stream.object()), + buffer.data(), written)}; + if (bytes < 0) + return Nothing; + + buffer.data()[written]='\0'; + buffer.resize(written); + + return buffer; +} + + +/* + * MimeCryptoContext + */ + +Result<size_t> +MimeCryptoContext::import_keys(MimeStream& stream) +{ + GError *err{}; + auto res = g_mime_crypto_context_import_keys( + self(), GMIME_STREAM(stream.object()), &err); + + if (res < 0) + return Err(Error::Code::File, &err, + "error importing keys"); + + return Ok(static_cast<size_t>(res)); +} + +void +MimeCryptoContext::set_request_password(PasswordRequestFunc pw_func) +{ + static auto request_func = pw_func; + + g_mime_crypto_context_set_request_password( + self(), + [](GMimeCryptoContext *ctx, + const char *user_id, + const char *prompt, + gboolean reprompt, + GMimeStream *response, + GError **err) -> gboolean { + MimeStream mstream{MimeStream::make_from_stream(response)}; + + auto res = request_func(MimeCryptoContext(ctx), + std::string{user_id ? user_id : ""}, + std::string{prompt ? prompt : ""}, + !!reprompt, + mstream); + if (res) + return TRUE; + + res.error().fill_g_error(err); + return FALSE; + }); + +} + +Result<void> +MimeCryptoContext::setup_gpg_test(const std::string& testpath) +{ + /* setup clean environment for testing; inspired by gmime */ + + g_setenv ("GNUPGHOME", format("%s/.gnupg", testpath.c_str()).c_str(), 1); + + /* disable environment variables that gpg-agent uses for pinentry */ + g_unsetenv ("DBUS_SESSION_BUS_ADDRESS"); + g_unsetenv ("DISPLAY"); + g_unsetenv ("GPG_TTY"); + + if (g_mkdir_with_parents((testpath + "/.gnupg").c_str(), 0700) != 0) + return Err(Error::Code::File, + "failed to create gnupg dir; err=%d", errno); + + auto write_gpgfile=[&](const std::string& fname, const std::string& data) + -> Result<void> { + + GError *err{}; + std::string path{format("%s/%s", testpath.c_str(), fname.c_str())}; + if (!g_file_set_contents(path.c_str(), data.c_str(), data.size(), &err)) + return Err(Error::Code::File, &err, + "failed to write %s", path.c_str()); + else + return Ok(); + }; + + // some more elegant way? + if (auto&& res = write_gpgfile("gpg.conf", "pinentry-mode loopback\n"); !res) + return res; + if (auto&& res = write_gpgfile("gpgsm.conf", "disable-crl-checks\n")) + return res; + + return Ok(); +} + + +/* + * MimeMessage + */ + + + +static Result<MimeMessage> +make_from_stream(GMimeStream* &&stream/*consume*/) +{ + init_gmime(); + GMimeParser *parser{g_mime_parser_new_with_stream(stream)}; + g_object_unref(stream); + if (!parser) + return Err(Error::Code::Message, "cannot create mime parser"); + + GMimeMessage *gmime_msg{g_mime_parser_construct_message(parser, NULL)}; + g_object_unref(parser); + if (!gmime_msg) + return Err(Error::Code::Message, "message seems invalid"); + + auto mime_msg{MimeMessage{std::move(G_OBJECT(gmime_msg))}}; + g_object_unref(gmime_msg); + + return Ok(std::move(mime_msg)); +} + +Result<MimeMessage> +MimeMessage::make_from_file(const std::string& path) +{ + GError* err{}; + init_gmime(); + if (auto&& stream{g_mime_stream_file_open(path.c_str(), "r", &err)}; !stream) + return Err(Error::Code::Message, &err, + "failed to open stream for %s", path.c_str()); + else + return make_from_stream(std::move(stream)); +} + +Result<MimeMessage> +MimeMessage::make_from_text(const std::string& text) +{ + init_gmime(); + if (auto&& stream{g_mime_stream_mem_new_with_buffer( + text.c_str(), text.length())}; !stream) + return Err(Error::Code::Message, + "failed to open stream for string"); + else + return make_from_stream(std::move(stream)); +} + +Option<int64_t> +MimeMessage::date() const noexcept +{ + GDateTime *dt{g_mime_message_get_date(self())}; + if (!dt) + return Nothing; + else + return g_date_time_to_unix(dt); +} + +constexpr Option<GMimeAddressType> +address_type(Contact::Type ctype) +{ + switch(ctype) { + case Contact::Type::Bcc: + return GMIME_ADDRESS_TYPE_BCC; + case Contact::Type::Cc: + return GMIME_ADDRESS_TYPE_CC; + case Contact::Type::From: + return GMIME_ADDRESS_TYPE_FROM; + case Contact::Type::To: + return GMIME_ADDRESS_TYPE_TO; + case Contact::Type::ReplyTo: + return GMIME_ADDRESS_TYPE_REPLY_TO; + case Contact::Type::Sender: + return GMIME_ADDRESS_TYPE_SENDER; + case Contact::Type::None: + default: + return Nothing; + } +} + +static Mu::Contacts +all_contacts(const MimeMessage& msg) +{ + Contacts contacts; + + for (auto&& cctype: { + Contact::Type::Sender, + Contact::Type::From, + Contact::Type::ReplyTo, + Contact::Type::To, + Contact::Type::Cc, + Contact::Type::Bcc + }) { + auto addrs{msg.contacts(cctype)}; + std::move(addrs.begin(), addrs.end(), + std::back_inserter(contacts)); + } + + return contacts; +} + +Mu::Contacts +MimeMessage::contacts(Contact::Type ctype) const noexcept +{ + /* special case: get all */ + if (ctype == Contact::Type::None) + return all_contacts(*this); + + const auto atype{address_type(ctype)}; + if (!atype) + return {}; + + auto addrs{g_mime_message_get_addresses(self(), *atype)}; + if (!addrs) + return {}; + + const auto msgtime{date().value_or(0)}; + + Contacts contacts; + auto lst_len{internet_address_list_length(addrs)}; + contacts.reserve(lst_len); + for (auto i = 0; i != lst_len; ++i) { + + auto&& addr{internet_address_list_get_address(addrs, i)}; + const auto name{internet_address_get_name(addr)}; + + if (G_UNLIKELY(!INTERNET_ADDRESS_IS_MAILBOX(addr))) + continue; + + const auto email{internet_address_mailbox_get_addr ( + INTERNET_ADDRESS_MAILBOX(addr))}; + if (G_UNLIKELY(!email)) + continue; + + contacts.emplace_back(email, name ? name : "", ctype, msgtime); + } + + return contacts; +} + +/* + * references() returns the concatenation of the References and In-Reply-To + * message-ids (in that order). Duplicates are removed. + * + * The _first_ one in the list determines the thread-id for the message. + */ +std::vector<std::string> +MimeMessage::references() const noexcept +{ + // is ref already in the list? O(n) but with small n. + auto is_dup = [](auto&& seq, const std::string& ref) { + return seq_some(seq, [&](auto&& str) { return ref == str; }); + }; + + auto is_fake = [](auto&& msgid) { + // this is bit ugly; protonmail injects fake References which + // can otherwise screw up threading. + if (g_str_has_suffix(msgid, "protonmail.internalid")) + return true; + /* ... */ + return false; + }; + + std::vector<std::string> refs; + for (auto&& ref_header: { "References", "In-reply-to" }) { + + auto hdr{header(ref_header)}; + if (!hdr) + continue; + + GMimeReferences *mime_refs{g_mime_references_parse({}, hdr->c_str())}; + refs.reserve(refs.size() + g_mime_references_length(mime_refs)); + + for (auto i = 0; i != g_mime_references_length(mime_refs); ++i) { + const auto msgid{g_mime_references_get_message_id(mime_refs, i)}; + if (msgid && !is_dup(refs, msgid) && !is_fake(msgid)) + refs.emplace_back(msgid); + } + g_mime_references_free(mime_refs); + } + + return refs; +} + +void +MimeMessage::for_each(const ForEachFunc& func) const noexcept +{ + struct CallbackData { const ForEachFunc& func; }; + CallbackData cbd{func}; + + g_mime_message_foreach( + self(), + [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) { + auto cb_data{reinterpret_cast<CallbackData*>(user_data)}; + cb_data->func(MimeObject{parent}, MimeObject{part}); + }, &cbd); +} + + + +/* + * MimePart + */ +size_t +MimePart::size() const noexcept +{ + auto wrapper{g_mime_part_get_content(self())}; + if (!wrapper) { + g_warning("failed to get content wrapper"); + return 0; + } + + auto stream{g_mime_data_wrapper_get_stream(wrapper)}; + if (!stream) { + g_warning("failed to get stream"); + return 0; + } + + return static_cast<size_t>(g_mime_stream_length(stream)); +} +Option<std::string> +MimePart::to_string() const noexcept +{ + /* + * easy case: text. this automatically handles conversion to utf-8. + */ + if (GMIME_IS_TEXT_PART(self())) { + if (char* txt{g_mime_text_part_get_text(GMIME_TEXT_PART(self()))}; !txt) + return Nothing; + else + return to_string_gchar(std::move(txt)/*consumes*/); + } + + /* + * harder case: read from stream manually + */ + GMimeDataWrapper *wrapper{g_mime_part_get_content(self())}; + if (!wrapper) { /* this happens with invalid mails */ + g_debug("failed to create data wrapper"); + return Nothing; + } + + GMimeStream *stream{g_mime_stream_mem_new()}; + if (!stream) { + g_warning("failed to create mem stream"); + return Nothing; + } + + ssize_t buflen{g_mime_data_wrapper_write_to_stream(wrapper, stream)}; + if (buflen <= 0) { /* empty buffer, not an error */ + g_object_unref(stream); + return Nothing; + } + + std::string buffer; + buffer.resize(buflen + 1); + g_mime_stream_reset(stream); + + auto bytes{g_mime_stream_read(stream, buffer.data(), buflen)}; + g_object_unref(stream); + if (bytes < 0) + return Nothing; + + buffer.data()[bytes]='\0'; + buffer.resize(buflen); + + return buffer; + +} + + + +Result<size_t> +MimePart::to_file(const std::string& path, bool overwrite) const noexcept +{ + MimeDataWrapper wrapper{g_mime_part_get_content(self())}; + if (!wrapper) /* this happens with invalid mails */ + return Err(Error::Code::File, "failed to create data wrapper"); + + GError *err{}; + auto strm{g_mime_stream_fs_open(path.c_str(), + O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL), + S_IRUSR|S_IWUSR, + &err)}; + if (!strm) + return Err(Error::Code::File, &err, "failed to open '%s'", path.c_str()); + + MimeStream stream{MimeStream::make_from_stream(strm)}; + ssize_t written{g_mime_data_wrapper_write_to_stream( + GMIME_DATA_WRAPPER(wrapper.object()), + GMIME_STREAM(stream.object()))}; + + if (written < 0) { + return Err(Error::Code::File, &err, + "failed to write to '%s'", path.c_str()); + } + + return Ok(static_cast<size_t>(written)); +} + + + + +void +MimeMultipart::for_each(const ForEachFunc& func) const noexcept +{ + struct CallbackData { const ForEachFunc& func; }; + CallbackData cbd{func}; + + g_mime_multipart_foreach( + self(), + [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) { + auto cb_data{reinterpret_cast<CallbackData*>(user_data)}; + cb_data->func(MimeObject{parent}, MimeObject{part}); + }, &cbd); +} + + +/* + * we need to be able to pass a crypto-context to the verify(), but + * g_mime_multipart_signed_verify() doesn't offer that anymore in GMime 3.x. + * + * So, add that by reimplementing it a bit (follow the upstream impl) + */ + + +static bool +mime_types_equal (const std::string& mime_type, const std::string& official_type) +{ + if (g_ascii_strcasecmp(mime_type.c_str(), official_type.c_str()) == 0) + return true; + + const auto slash_pos = official_type.find("/"); + if (slash_pos == std::string::npos || slash_pos == 0) + return false; + + /* If the official mime-type's subtype already begins with "x-", then there's + * nothing else to check. */ + const auto subtype{official_type.substr(slash_pos + 1)}; + if (g_ascii_strncasecmp (subtype.c_str(), "x-", 2) == 0) + return false; + const auto supertype{official_type.substr(0, slash_pos - 1)}; + const auto xtype{official_type.substr(0, slash_pos - 1) + "x-" + subtype}; + + /* Check if the "x-" version of the official mime-type matches the + * supplied mime-type. For example, if the official mime-type is + * "application/pkcs7-signature", then we also want to match + * "application/x-pkcs7-signature". */ + return g_ascii_strcasecmp(mime_type.c_str(), xtype.c_str()) == 0; +} + + +/** + * A bit of a monster, this impl. + * + * It's the transliteration of the g_mime_multipart_signed_verify() which + * adds the feature of passing in the CryptoContext. + * + */ +Result<std::vector<MimeSignature>> +MimeMultipartSigned::verify(const MimeCryptoContext& ctx, VerifyFlags vflags) const noexcept +{ + if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2) + return Err(Error::Code::Crypto, "cannot verify, not enough subparts"); + + const auto proto{content_type_parameter("protocol")}; + const auto sign_proto{ctx.signature_protocol()}; + + if (!proto || !sign_proto || !mime_types_equal(*proto, *sign_proto)) + return Err(Error::Code::Crypto, "unsupported protocol " + + proto.value_or("<unknown>")); + + const auto sig{signed_signature_part()}; + const auto content{signed_content_part()}; + if (!sig || !content) + return Err(Error::Code::Crypto, "cannot find part"); + + const auto sig_mime_type{sig->mime_type()}; + if (!sig || !mime_types_equal(sig_mime_type.value_or("<none>"), *sign_proto)) + return Err(Error::Code::Crypto, "failed to find matching signature part"); + + MimeFormatOptions fopts{g_mime_format_options_new()}; + g_mime_format_options_set_newline_format(fopts.get(), GMIME_NEWLINE_FORMAT_DOS); + + MimeStream stream{MimeStream::make_mem()}; + if (auto&& res = content->write_to_stream(fopts, stream); !res) + return Err(res.error()); + stream.reset(); + + MimeDataWrapper wrapper{g_mime_part_get_content(GMIME_PART(sig->object()))}; + MimeStream sigstream{MimeStream::make_mem()}; + if (auto&& res = wrapper.write_to_stream(sigstream); !res) + return Err(res.error()); + sigstream.reset(); + + GError *err{}; + GMimeSignatureList *siglist{g_mime_crypto_context_verify( + GMIME_CRYPTO_CONTEXT(ctx.object()), + static_cast<GMimeVerifyFlags>(vflags), + GMIME_STREAM(stream.object()), + GMIME_STREAM(sigstream.object()), + {}, + &err)}; + if (!siglist) + return Err(Error::Code::Crypto, &err, "failed to verify"); + + std::vector<MimeSignature> sigs; + for (auto i = 0; + i != g_mime_signature_list_length(siglist); ++i) { + GMimeSignature *msig = g_mime_signature_list_get_signature(siglist, i); + sigs.emplace_back(MimeSignature(msig)); + } + g_object_unref(siglist); + + return sigs; +} + + +std::vector<MimeCertificate> +MimeDecryptResult::recipients() const noexcept +{ + GMimeCertificateList *lst{g_mime_decrypt_result_get_recipients(self())}; + if (!lst) + return {}; + + std::vector<MimeCertificate> certs; + for (int i = 0; i != g_mime_certificate_list_length(lst); ++i) + certs.emplace_back( + MimeCertificate( + g_mime_certificate_list_get_certificate(lst, i))); + + return certs; +} + +std::vector<MimeSignature> +MimeDecryptResult::signatures() const noexcept +{ + GMimeSignatureList *lst{g_mime_decrypt_result_get_signatures(self())}; + if (!lst) + return {}; + + std::vector<MimeSignature> sigs; + for (auto i = 0; i != g_mime_signature_list_length(lst); ++i) { + GMimeSignature *sig = g_mime_signature_list_get_signature(lst, i); + sigs.emplace_back(MimeSignature(sig)); + } + + return sigs; +} +/** + * Like verify, a bit of a monster, this impl. + * + * It's the transliteration of the g_mime_multipart_encrypted_decrypt() which + * adds the feature of passing in the CryptoContext. + * + */ + +Mu::Result<MimeMultipartEncrypted::Decrypted> +MimeMultipartEncrypted::decrypt(const MimeCryptoContext& ctx, DecryptFlags dflags, + const std::string& session_key) const noexcept +{ + if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2) + return Err(Error::Code::Crypto, "cannot decrypted, not enough subparts"); + + const auto proto{content_type_parameter("protocol")}; + const auto enc_proto{ctx.encryption_protocol()}; + + if (!proto || !enc_proto || !mime_types_equal(*proto, *enc_proto)) + return Err(Error::Code::Crypto, "unsupported protocol " + + proto.value_or("<unknown>")); + + const auto version{encrypted_version_part()}; + const auto encrypted{encrypted_content_part()}; + if (!version || !encrypted) + return Err(Error::Code::Crypto, "cannot find part"); + + if (!mime_types_equal(version->mime_type().value_or(""), proto.value())) + return Err(Error::Code::Crypto, + "cannot decrypt; unexpected version content-type '%s' != '%s'", + version->mime_type().value_or("").c_str(), + proto.value().c_str()); + + if (!mime_types_equal(encrypted->mime_type().value_or(""), "application/octet-stream")) + return Err(Error::Code::Crypto, + "cannot decrypt; unexpected encrypted content-type '%s'", + encrypted->mime_type().value_or("").c_str()); + + const auto content{encrypted->content()}; + auto ciphertext{MimeStream::make_mem()}; + content.write_to_stream(ciphertext); + ciphertext.reset(); + + auto stream{MimeStream::make_mem()}; + auto filtered{MimeStream::make_filtered(stream)}; + auto filter{g_mime_filter_dos2unix_new(FALSE)}; + g_mime_stream_filter_add(GMIME_STREAM_FILTER(filtered.object()), + filter); + g_object_unref(filter); + + GError *err{}; + GMimeDecryptResult *dres = + g_mime_crypto_context_decrypt(GMIME_CRYPTO_CONTEXT(ctx.object()), + static_cast<GMimeDecryptFlags>(dflags), + session_key.empty() ? + NULL : session_key.c_str(), + GMIME_STREAM(ciphertext.object()), + GMIME_STREAM(filtered.object()), + &err); + if (!dres) + return Err(Error::Code::Crypto, &err, "decryption failed"); + + filtered.flush(); + stream.reset(); + + auto parser{g_mime_parser_new()}; + g_mime_parser_init_with_stream(parser, GMIME_STREAM(stream.object())); + + auto decrypted{g_mime_parser_construct_part(parser, NULL)}; + g_object_unref(parser); + if (!decrypted) { + g_object_unref(dres); + return Err(Error::Code::Crypto, "failed to parse decrypted part"); + } + + Decrypted result = { MimeObject{decrypted}, MimeDecryptResult{dres} }; + + g_object_unref(decrypted); + g_object_unref(dres); + + return Ok(std::move(result)); +} diff --git a/lib/message/mu-mime-object.hh b/lib/message/mu-mime-object.hh new file mode 100644 index 0000000..56a4b56 --- /dev/null +++ b/lib/message/mu-mime-object.hh @@ -0,0 +1,1377 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_MIME_OBJECT_HH__ +#define MU_MIME_OBJECT_HH__ + +#include <stdexcept> +#include <string> +#include <functional> +#include <array> +#include <vector> +#include <gmime/gmime.h> +#include "gmime/gmime-application-pkcs7-mime.h" +#include "gmime/gmime-crypto-context.h" +#include "utils/mu-option.hh" +#include "utils/mu-result.hh" +#include "utils/mu-utils.hh" +#include "mu-contact.hh" + +namespace Mu { + +/* non-GObject types */ + +using MimeFormatOptions = deletable_unique_ptr<GMimeFormatOptions, g_mime_format_options_free>; + +/** + * Initialize gmime (idempotent) + * + */ +void init_gmime(void); + + +/** + * Get a RFC2047-compatible address for the given contact + * + * @param contact a contact + * + * @return an address string + */ +std::string address_rfc2047(const Contact& contact); + +class Object { +public: + /** + * Default CTOR + * + */ + Object() noexcept: self_{} {} + + /** + * Create an object from a GObject + * + * @param obj a gobject. A ref is added. + */ + Object(GObject* &&obj): self_{G_OBJECT(g_object_ref(obj))} { + if (!G_IS_OBJECT(obj)) + throw std::runtime_error("not a g-object"); + } + + /** + * Copy CTOR + * + * @param other some other Object + */ + Object(const Object& other) noexcept { *this = other; } + + /** + * Move CTOR + * + * @param other some other Object + */ + Object(Object&& other) noexcept { *this = std::move(other); } + + /** + * operator= + * + * @param other copy some other object + * + * @return *this + */ + Object& operator=(const Object& other) noexcept { + + if (this != &other) { + auto oldself = self_; + self_ = other.self_ ? + G_OBJECT(g_object_ref(other.self_)) : nullptr; + if (oldself) + g_object_unref(oldself); + } + return *this; + } + + /** + * operator= + * + * @param other move some object object + * + * @return + */ + Object& operator=(Object&& other) noexcept { + + if (this != &other) { auto oldself = self_; + self_ = other.self_; + other.self_ = nullptr; + if (oldself) + g_object_unref(oldself); + } + return *this; + } + + /** + * DTOR + */ + virtual ~Object() { + if (self_) { + g_object_unref(self_); + } + } + + /** + * operator bool + * + * @return true if object wraps a GObject, false otherwise + */ + operator bool() const noexcept { return !!self_; } + + /** + * Get a ptr to the underlying GObject + * + * @return GObject or NULL + */ + GObject* object() const { return self_; } + + + /** + * Unref the object + * + */ + void unref() noexcept { + g_object_unref(self_); + } + + + /** + * Ref the object + * + */ + void ref() noexcept { + g_object_ref(self_); + } + + +private: + mutable GObject *self_{}; +}; + + + + + + +/** + * Thin wrapper around a GMimeContentType + * + */ +struct MimeContentType: public Object { + + MimeContentType(GMimeContentType *ctype) : Object{G_OBJECT(ctype)} { + if (!GMIME_IS_CONTENT_TYPE(self())) + throw std::runtime_error("not a content-type"); + } + std::string media_type() const noexcept { + return g_mime_content_type_get_media_type(self()); + } + std::string media_subtype() const noexcept { + return g_mime_content_type_get_media_subtype(self()); + } + + Option<std::string> mime_type() const noexcept { + return to_string_opt_gchar(g_mime_content_type_get_mime_type(self())); + } + + bool is_type(const std::string& type, const std::string& subtype) const { + return g_mime_content_type_is_type(self(), type.c_str(), + subtype.c_str()); + } +private: + GMimeContentType* self() const { + return reinterpret_cast<GMimeContentType*>(object()); + } +}; + + + + + +/** + * Thin wrapper around a GMimeStream + * + */ +struct MimeStream: public Object { + + ssize_t write(const char* buf, ::size_t size) { + return g_mime_stream_write(self(), buf, size); + } + + bool reset() { + return g_mime_stream_reset(self()) < 0 ? false : true; + } + + bool flush() { + return g_mime_stream_flush(self()) < 0 ? false : true; + } + + static MimeStream make_mem() { + MimeStream mstream{g_mime_stream_mem_new()}; + mstream.unref(); /* remove extra ref */ + return mstream; + } + + static MimeStream make_filtered(MimeStream& stream) { + MimeStream mstream{g_mime_stream_filter_new(stream.self())}; + mstream.unref(); /* remove extra refs */ + return mstream; + } + + static MimeStream make_from_stream(GMimeStream *strm) { + MimeStream mstream{strm}; + mstream.unref(); /* remove extra ref */ + return mstream; + } + +private: + MimeStream(GMimeStream *stream): Object(G_OBJECT(stream)) { + if (!GMIME_IS_STREAM(self())) + throw std::runtime_error("not a mime-stream"); + }; + + GMimeStream* self() const { + return reinterpret_cast<GMimeStream*>(object()); + } +}; + +template<typename S, typename T> +constexpr Option<std::string_view> to_string_view_opt(const S& seq, T t) { + auto&& it = seq_find_if(seq, [&](auto&& item){return item.first == t;}); + if (it == seq.cend()) + return Nothing; + else + return it->second; +} + + +/** + * Thin wrapper around a GMimeDataWrapper + * + */ +struct MimeDataWrapper: public Object { + MimeDataWrapper(GMimeDataWrapper *wrapper): Object(G_OBJECT(wrapper)) { + if (!GMIME_IS_DATA_WRAPPER(self())) + throw std::runtime_error("not a data-wrapper"); + }; + + Result<size_t> write_to_stream(MimeStream& stream) const { + if (auto&& res = g_mime_data_wrapper_write_to_stream( + self(), GMIME_STREAM(stream.object())) ; res < 0) + return Err(Error::Code::Message, "failed to write to stream"); + else + return Ok(static_cast<size_t>(res)); + } + +private: + GMimeDataWrapper* self() const { + return reinterpret_cast<GMimeDataWrapper*>(object()); + } +}; + + + +/** + * Thin wrapper around a GMimeCertifcate + * + */ +struct MimeCertificate: public Object { + MimeCertificate(GMimeCertificate *cert) : Object{G_OBJECT(cert)} { + if (!GMIME_IS_CERTIFICATE(self())) + throw std::runtime_error("not a certificate"); + } + + enum struct PubkeyAlgo { + Default = GMIME_PUBKEY_ALGO_DEFAULT, + Rsa = GMIME_PUBKEY_ALGO_RSA, + RsaE = GMIME_PUBKEY_ALGO_RSA_E, + RsaS = GMIME_PUBKEY_ALGO_RSA_S, + ElgE = GMIME_PUBKEY_ALGO_ELG_E, + Dsa = GMIME_PUBKEY_ALGO_DSA, + Ecc = GMIME_PUBKEY_ALGO_ECC, + Elg = GMIME_PUBKEY_ALGO_ELG, + EcDsa = GMIME_PUBKEY_ALGO_ECDSA, + EcDh = GMIME_PUBKEY_ALGO_ECDH, + EdDsa = GMIME_PUBKEY_ALGO_EDDSA, + }; + + enum struct DigestAlgo { + Default = GMIME_DIGEST_ALGO_DEFAULT, + Md5 = GMIME_DIGEST_ALGO_MD5, + Sha1 = GMIME_DIGEST_ALGO_SHA1, + RipEmd160 = GMIME_DIGEST_ALGO_RIPEMD160, + Md2 = GMIME_DIGEST_ALGO_MD2, + Tiger192 = GMIME_DIGEST_ALGO_TIGER192, + Haval5160 = GMIME_DIGEST_ALGO_HAVAL5160, + Sha256 = GMIME_DIGEST_ALGO_SHA256, + Sha384 = GMIME_DIGEST_ALGO_SHA384, + Sha512 = GMIME_DIGEST_ALGO_SHA512, + Sha224 = GMIME_DIGEST_ALGO_SHA224, + Md4 = GMIME_DIGEST_ALGO_MD4, + Crc32 = GMIME_DIGEST_ALGO_CRC32, + Crc32Rfc1510 = GMIME_DIGEST_ALGO_CRC32_RFC1510, + Crc32Rfc2440 = GMIME_DIGEST_ALGO_CRC32_RFC2440, + }; + + enum struct Trust { + Unknown = GMIME_TRUST_UNKNOWN, + Undefined = GMIME_TRUST_UNDEFINED, + Never = GMIME_TRUST_NEVER, + Marginal = GMIME_TRUST_MARGINAL, + TrustFull = GMIME_TRUST_FULL, + TrustUltimate = GMIME_TRUST_ULTIMATE, + }; + + enum struct Validity { + Unknown = GMIME_VALIDITY_UNKNOWN, + Undefined = GMIME_VALIDITY_UNDEFINED, + Never = GMIME_VALIDITY_NEVER, + Marginal = GMIME_VALIDITY_MARGINAL, + Full = GMIME_VALIDITY_FULL, + Ultimate = GMIME_VALIDITY_ULTIMATE, + }; + + PubkeyAlgo pubkey_algo() const { + return static_cast<PubkeyAlgo>( + g_mime_certificate_get_pubkey_algo(self())); + } + + DigestAlgo digest_algo() const { + return static_cast<DigestAlgo>( + g_mime_certificate_get_digest_algo(self())); + } + + Validity id_validity() const { + return static_cast<Validity>( + g_mime_certificate_get_id_validity(self())); + } + + Trust trust() const { + return static_cast<Trust>( + g_mime_certificate_get_trust(self())); + } + + Option<std::string> issuer_serial() const { + return to_string_opt(g_mime_certificate_get_issuer_serial(self())); + } + Option<std::string> issuer_name() const { + return to_string_opt(g_mime_certificate_get_issuer_name(self())); + } + + Option<std::string> fingerprint() const { + return to_string_opt(g_mime_certificate_get_fingerprint(self())); + } + + Option<std::string> key_id() const { + return to_string_opt(g_mime_certificate_get_key_id(self())); + } + + + Option<std::string> name() const { + return to_string_opt(g_mime_certificate_get_name(self())); + } + + Option<std::string> user_id() const { + return to_string_opt(g_mime_certificate_get_user_id(self())); + } + + Option<::time_t> created() const { + if (auto t = g_mime_certificate_get_created(self()); t >= 0) + return t; + else + return Nothing; + } + + Option<::time_t> expires() const { + if (auto t = g_mime_certificate_get_expires(self()); t >= 0) + return t; + else + return Nothing; + } + +private: + GMimeCertificate* self() const { + return reinterpret_cast<GMimeCertificate*>(object()); + } +}; + +constexpr std::array<std::pair<MimeCertificate::PubkeyAlgo, std::string_view>, 11> +AllPubkeyAlgos = {{ + { MimeCertificate::PubkeyAlgo::Default, "default"}, + { MimeCertificate::PubkeyAlgo::Rsa, "rsa"}, + { MimeCertificate::PubkeyAlgo::RsaE, "rsa-encryption-only"}, + { MimeCertificate::PubkeyAlgo::RsaS, "rsa-signing-only"}, + { MimeCertificate::PubkeyAlgo::ElgE, "el-gamal-encryption-only"}, + { MimeCertificate::PubkeyAlgo::Dsa, "dsa"}, + { MimeCertificate::PubkeyAlgo::Ecc, "elliptic curve"}, + { MimeCertificate::PubkeyAlgo::Elg, "el-gamal"}, + { MimeCertificate::PubkeyAlgo::EcDsa, "elliptic-curve+dsa"}, + { MimeCertificate::PubkeyAlgo::EcDh, "elliptic-curve+diffie-helman"}, + { MimeCertificate::PubkeyAlgo::EdDsa, "elliptic-curve+dsa-2"} + }}; + +constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::PubkeyAlgo algo) { + return to_string_view_opt(AllPubkeyAlgos, algo); +} + +constexpr std::array<std::pair<MimeCertificate::DigestAlgo, std::string_view>, 15> +AllDigestAlgos = {{ + { MimeCertificate::DigestAlgo::Default, "default"}, + { MimeCertificate::DigestAlgo::Md5, "md5"}, + { MimeCertificate::DigestAlgo::Sha1, "sha1"}, + { MimeCertificate::DigestAlgo::RipEmd160, "ripemd-160"}, + { MimeCertificate::DigestAlgo::Md2, "md2"}, + { MimeCertificate::DigestAlgo::Tiger192, "tiger-192"}, + { MimeCertificate::DigestAlgo::Haval5160, "haval-5-160"}, + { MimeCertificate::DigestAlgo::Sha256, "sha-256"}, + { MimeCertificate::DigestAlgo::Sha384, "sha-384"}, + { MimeCertificate::DigestAlgo::Sha512, "sha-512"}, + { MimeCertificate::DigestAlgo::Sha224, "sha-224"}, + { MimeCertificate::DigestAlgo::Md4, "md4"}, + { MimeCertificate::DigestAlgo::Crc32, "crc32"}, + { MimeCertificate::DigestAlgo::Crc32Rfc1510, "crc32-rfc1510"}, + { MimeCertificate::DigestAlgo::Crc32Rfc2440, "crc32-rfc2440"}, + }}; + +constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::DigestAlgo algo) { + return to_string_view_opt(AllDigestAlgos, algo); +} + +constexpr std::array<std::pair<MimeCertificate::Trust, std::string_view>, 6> +AllTrusts = {{ + { MimeCertificate::Trust::Unknown, "unknown" }, + { MimeCertificate::Trust::Undefined, "undefined" }, + { MimeCertificate::Trust::Never, "never" }, + { MimeCertificate::Trust::Marginal, "marginal" }, + { MimeCertificate::Trust::TrustFull, "trust-full" }, + { MimeCertificate::Trust::TrustUltimate,"trust-ultimate" }, + }}; + +constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Trust trust) { + return to_string_view_opt(AllTrusts, trust); +} + +constexpr std::array<std::pair<MimeCertificate::Validity, std::string_view>, 6> +AllValidities = {{ + { MimeCertificate::Validity::Unknown, "unknown" }, + { MimeCertificate::Validity::Undefined, "undefined" }, + { MimeCertificate::Validity::Never, "never" }, + { MimeCertificate::Validity::Marginal, "marginal" }, + { MimeCertificate::Validity::Full, "full" }, + { MimeCertificate::Validity::Ultimate, "ultimate" }, + }}; + +constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Validity val) { + return to_string_view_opt(AllValidities, val); +} + + + +/** + * Thin wrapper around a GMimeSignature + * + */ +struct MimeSignature: public Object { + MimeSignature(GMimeSignature *sig) : Object{G_OBJECT(sig)} { + if (!GMIME_IS_SIGNATURE(self())) + throw std::runtime_error("not a signature"); + } + + /** + * Signature status + * + */ + enum struct Status { + Valid = GMIME_SIGNATURE_STATUS_VALID, + Green = GMIME_SIGNATURE_STATUS_GREEN, + Red = GMIME_SIGNATURE_STATUS_RED, + KeyRevoked = GMIME_SIGNATURE_STATUS_KEY_REVOKED, + KeyExpired = GMIME_SIGNATURE_STATUS_KEY_EXPIRED, + SigExpired = GMIME_SIGNATURE_STATUS_SIG_EXPIRED, + KeyMissing = GMIME_SIGNATURE_STATUS_KEY_MISSING, + CrlMissing = GMIME_SIGNATURE_STATUS_CRL_MISSING, + CrlTooOld = GMIME_SIGNATURE_STATUS_CRL_TOO_OLD, + BadPolicy = GMIME_SIGNATURE_STATUS_BAD_POLICY, + SysError = GMIME_SIGNATURE_STATUS_SYS_ERROR, + TofuConflict = GMIME_SIGNATURE_STATUS_TOFU_CONFLICT + }; + + Status status() const { return static_cast<Status>( + g_mime_signature_get_status(self())); } + + ::time_t created() const { return g_mime_signature_get_created(self()); } + ::time_t expires() const { return g_mime_signature_get_expires(self()); } + + + const MimeCertificate certificate() const { + return MimeCertificate{g_mime_signature_get_certificate(self())}; + } + +private: + GMimeSignature* self() const { + return reinterpret_cast<GMimeSignature*>(object()); + } +}; + +constexpr std::array<std::pair<MimeSignature::Status, std::string_view>, 12> +AllMimeSignatureStatuses= {{ + { MimeSignature::Status::Valid, "valid" }, + { MimeSignature::Status::Green, "green" }, + { MimeSignature::Status::Red, "red" }, + { MimeSignature::Status::KeyRevoked, "key-revoked" }, + { MimeSignature::Status::KeyExpired, "key-expired" }, + { MimeSignature::Status::SigExpired, "sig-expired" }, + { MimeSignature::Status::KeyMissing, "key-missing" }, + { MimeSignature::Status::CrlMissing, "crl-missing" }, + { MimeSignature::Status::CrlTooOld, "crl-too-old" }, + { MimeSignature::Status::BadPolicy, "bad-policy" }, + { MimeSignature::Status::SysError, "sys-error" }, + { MimeSignature::Status::TofuConflict, "tofu-confict" }, + }}; +MU_ENABLE_BITOPS(MimeSignature::Status); + +static inline std::string to_string(MimeSignature::Status status) { + std::string str; + for (auto&& item: AllMimeSignatureStatuses) { + if (none_of(item.first & status)) + continue; + if (!str.empty()) + str += ", "; + str += item.second; + } + if (str.empty()) + str = "none"; + + return str; +} + + + + +/** +* Thin wrapper around a GMimeDecryptResult + * + */ +struct MimeDecryptResult: public Object { + MimeDecryptResult (GMimeDecryptResult *decres) : Object{G_OBJECT(decres)} { + if (!GMIME_IS_DECRYPT_RESULT(self())) + throw std::runtime_error("not a decrypt-result"); + } + + std::vector<MimeCertificate> recipients() const noexcept; + std::vector<MimeSignature> signatures() const noexcept; + + enum struct CipherAlgo { + Default = GMIME_CIPHER_ALGO_DEFAULT, + Idea = GMIME_CIPHER_ALGO_IDEA, + Des3 = GMIME_CIPHER_ALGO_3DES, + Cast5 = GMIME_CIPHER_ALGO_CAST5, + Blowfish = GMIME_CIPHER_ALGO_BLOWFISH, + Aes = GMIME_CIPHER_ALGO_AES, + Aes192 = GMIME_CIPHER_ALGO_AES192, + Aes256 = GMIME_CIPHER_ALGO_AES256, + TwoFish = GMIME_CIPHER_ALGO_TWOFISH, + Camellia128 = GMIME_CIPHER_ALGO_CAMELLIA128, + Camellia192 = GMIME_CIPHER_ALGO_CAMELLIA192, + Camellia256 = GMIME_CIPHER_ALGO_CAMELLIA256 + }; + + CipherAlgo cipher() const noexcept { + return static_cast<CipherAlgo>( + g_mime_decrypt_result_get_cipher(self())); + } + + using DigestAlgo = MimeCertificate::DigestAlgo; + DigestAlgo mdc() const noexcept { + return static_cast<DigestAlgo>( + g_mime_decrypt_result_get_mdc(self())); + } + + Option<std::string> session_key() const noexcept { + return to_string_opt(g_mime_decrypt_result_get_session_key(self())); + } + +private: + GMimeDecryptResult* self() const { + return reinterpret_cast<GMimeDecryptResult*>(object()); + } +}; + +constexpr std::array<std::pair<MimeDecryptResult::CipherAlgo, std::string_view>, 12> +AllCipherAlgos= {{ + {MimeDecryptResult::CipherAlgo::Default, "default"}, + {MimeDecryptResult::CipherAlgo::Idea, "idea"}, + {MimeDecryptResult::CipherAlgo::Des3, "3des"}, + {MimeDecryptResult::CipherAlgo::Cast5, "cast5"}, + {MimeDecryptResult::CipherAlgo::Blowfish, "blowfish"}, + {MimeDecryptResult::CipherAlgo::Aes, "aes"}, + {MimeDecryptResult::CipherAlgo::Aes192, "aes192"}, + {MimeDecryptResult::CipherAlgo::Aes256, "aes256"}, + {MimeDecryptResult::CipherAlgo::TwoFish, "twofish"}, + {MimeDecryptResult::CipherAlgo::Camellia128, "camellia128"}, + {MimeDecryptResult::CipherAlgo::Camellia192, "camellia192"}, + {MimeDecryptResult::CipherAlgo::Camellia256, "camellia256"}, + }}; + +constexpr Option<std::string_view> to_string_view_opt(MimeDecryptResult::CipherAlgo algo) { + return to_string_view_opt(AllCipherAlgos, algo); +} + + +/** + * Thin wrapper around a GMimeCryptoContext + * + */ +struct MimeCryptoContext : public Object { + + /** + * Make a new PGP crypto context. + * + * For 'test-mode', pass a test-path; in this mode GPG will be setup + * in an isolated mode so it does not affect normal usage. + * + * @param testpath (for unit-tests) pass a path to an existing dir to + * create a pgp setup. For normal use, leave empty. + * + * @return A MimeCryptoContext or an error + */ + static Result<MimeCryptoContext> + make_gpg(const std::string& testpath={}) try { + if (!testpath.empty()) { + if (auto&& res = setup_gpg_test(testpath); !res) + return Err(res.error()); + } + MimeCryptoContext ctx(g_mime_gpg_context_new()); + ctx.unref(); /* remove extra ref */ + return Ok(std::move(ctx)); + } catch (...) { + return Err(Error::Code::Crypto, "failed to create crypto context"); + } + + static Result<MimeCryptoContext> + make(const std::string& protocol) { + auto ctx = g_mime_crypto_context_new(protocol.c_str()); + if (!ctx) + return Err(Error::Code::Crypto, "unsupported protocol " + protocol); + MimeCryptoContext mctx{ctx}; + mctx.unref(); /* remove extra ref */ + return Ok(std::move(mctx)); + } + + Option<std::string> encryption_protocol() const noexcept { + return to_string_opt(g_mime_crypto_context_get_encryption_protocol(self())); + } + Option<std::string> signature_protocol() const noexcept { + return to_string_opt(g_mime_crypto_context_get_signature_protocol(self())); + } + Option<std::string> key_exchange_protocol() const noexcept { + return to_string_opt(g_mime_crypto_context_get_key_exchange_protocol(self())); + } + + /** + * Imports a stream of keys/certificates contained within stream into + * the key/certificate database controlled by @this. + * + * @param stream + * + * @return number of keys imported, or an error. + */ + Result<size_t> import_keys(MimeStream& stream); + + /** + * Prototype for a request-password function. + * + * @param ctx the MimeCryptoContext making the request + * @param user_id the user_id of the password being requested + * @param prompt a string containing some helpful context for the prompt + * @param reprompt true if this password request is a reprompt due to a + * previously bad password response + * @param response a stream for the application to write the password to + * (followed by a newline '\n' character) + * + * @return nothing (Ok) or an error, + */ + using PasswordRequestFunc = + std::function<Result<void>( + const MimeCryptoContext& ctx, + const std::string& user_id, + const std::string& prompt, + bool reprompt, + MimeStream& response)>; + /** + * Set a function to request a password. + * + * @param pw_func password function. + */ + void set_request_password(PasswordRequestFunc pw_func); + + +private: + MimeCryptoContext(GMimeCryptoContext *ctx): Object{G_OBJECT(ctx)} { + if (!GMIME_IS_CRYPTO_CONTEXT(self())) + throw std::runtime_error("not a crypto-context"); + } + + static Result<void> setup_gpg_test(const std::string& testpath); + + GMimeCryptoContext* self() const { + return reinterpret_cast<GMimeCryptoContext*>(object()); + } +}; + + +/** + * Thin wrapper around a GMimeObject + * + */ +class MimeObject: public Object { +public: + /** + * Construct a new MimeObject. Take a ref on the obj + * + * @param mime_part mime-part pointer + */ + MimeObject(const Object& obj): Object{obj} { + if (!GMIME_IS_OBJECT(self())) + throw std::runtime_error("not a mime-object"); + } + MimeObject(GMimeObject *mobj): Object{G_OBJECT(mobj)} { + if (mobj && !GMIME_IS_OBJECT(self())) + throw std::runtime_error("not a mime-object"); + } + + /** + * Get a header from the MimeObject + * + * @param header the header to retrieve + * + * @return header value (UTF-8) or Nothing + */ + Option<std::string> header(const std::string& header) const noexcept; + + + /** + * Get all headers as pairs of name, value + * + * @return all headers + */ + std::vector<std::pair<std::string, std::string>> headers() const noexcept; + + + /** + * Get the content type + * + * @return the content-type or Nothing + */ + Option<MimeContentType> content_type() const noexcept { + auto ct{g_mime_object_get_content_type(self())}; + if (!ct) + return Nothing; + else + return MimeContentType(ct); + } + + Option<std::string> mime_type() const noexcept { + if (auto ct = content_type(); !ct) + return Nothing; + else + return ct->mime_type(); + } + + /** + * Get the content-type parameter + * + * @param param name of parameter + * + * @return the value of the parameter, or Nothing + */ + Option<std::string> content_type_parameter(const std::string& param) const noexcept { + return Mu::to_string_opt( + g_mime_object_get_content_type_parameter(self(), param.c_str())); + } + + /** + * Write this MimeObject to some stream + * + * @param f_opts formatting options + * @param stream the stream + * + * @return the number or bytes written or an error + */ + Result<size_t> write_to_stream(const MimeFormatOptions& f_opts, + MimeStream& stream) const; + /** + * Write the object to a string. + * + * @return + */ + Option<std::string> to_string_opt() const noexcept; + + /* + * subtypes. + */ + + /** + * Is this a MimePart? + * + * @return true or false + */ + bool is_part() const { return GMIME_IS_PART(self()); } + + /** + * Is this a MimeMultiPart? + * + * @return true or false + */ + bool is_multipart() const { return GMIME_IS_MULTIPART(self());} + + /** + * Is this a MimeMultiPart? + * + * @return true or false + */ + bool is_multipart_encrypted() const { + return GMIME_IS_MULTIPART_ENCRYPTED(self()); + } + + /** + * Is this a MimeMultiPart? + * + * @return true or false + */ + bool is_multipart_signed() const { + return GMIME_IS_MULTIPART_SIGNED(self()); + } + + /** + * Is this a MimeMessage? + * + * @return true or false + */ + bool is_message() const { return GMIME_IS_MESSAGE(self());} + + /** + * Is this a MimeMessagePart? + * + * @return true orf alse + */ + bool is_message_part() const { return GMIME_IS_MESSAGE_PART(self());} + + /** + * Is this a MimeApplicationpkcs7Mime? + * + * @return true orf alse + */ + bool is_mime_application_pkcs7_mime() const { + return GMIME_IS_APPLICATION_PKCS7_MIME(self()); + } + + /** + * Callback for for_each(). See GMimeObjectForEachFunc. + * + */ + using ForEachFunc = std::function<void(const MimeObject& parent, + const MimeObject& part)>; + +private: + GMimeObject* self() const { + return reinterpret_cast<GMimeObject*>(object()); + } +}; + + +/** + * Thin wrapper around a GMimeMessage + * + */ +class MimeMessage: public MimeObject { +public: + /** + * Construct a MimeMessage + * + * @param obj an Object of the right type + */ + MimeMessage(const Object& obj): MimeObject(obj) { + if (!is_message()) + throw std::runtime_error("not a mime-message"); + } + + /** + * Make a MimeMessage from a file + * + * @param path path to the file + * + * @return a MimeMessage or an error. + */ + static Result<MimeMessage> make_from_file (const std::string& path); + + /** + * Make a MimeMessage from a string + * + * @param path path to the file + * + * @return a MimeMessage or an error. + */ + static Result<MimeMessage> make_from_text (const std::string& text); + + /** + * Get the contacts of a given type, or None for _all_ + * + * @param ctype contact type + * + * @return contacts + */ + Contacts contacts(Contact::Type ctype) const noexcept; + + /** + * Gets the message-id if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option<std::string> message_id() const noexcept { + return Mu::to_string_opt(g_mime_message_get_message_id(self())); + } + + /** + * Gets the message-id if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option<std::string> subject() const noexcept { + return Mu::to_string_opt(g_mime_message_get_subject(self())); + } + + /** + * Gets the date if it exists, or nullopt otherwise. + * + * @return a time_t value (expressed as a 64-bit number) or nullopt + */ + Option<int64_t> date() const noexcept; + + + /** + * Get the references for this message (including in-reply-to), in the + * order of older..newer; the first one would the oldest parent, and + * in-reply-to would be the last one (if any). These are de-duplicated, + * and known-fake references removed (see implementation) + * + * @return references. + */ + std::vector<std::string> references() const noexcept; + + + /** + * Recursively apply func tol all parts of this message + * + * @param func a function + */ + void for_each(const ForEachFunc& func) const noexcept; + +private: + GMimeMessage* self() const { + return reinterpret_cast<GMimeMessage*>(object()); + } +}; + +/** + * Thin wrapper around a GMimePart. + * + */ +class MimePart: public MimeObject { +public: + /** + * Construct a MimePart + * + * @param obj an Object of the right type + */ + MimePart(const Object& obj): MimeObject(obj) { + if (!is_part()) + throw std::runtime_error("not a mime-part"); + } + + /** + * Determines whether or not the part is an attachment based on the + * value of the Content-Disposition header. + * + * @return true or false + */ + bool is_attachment() const noexcept { + return g_mime_part_is_attachment(self()); + } + + /** + * Gets the value of the Content-Description for this mime part + * if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option<std::string> content_description() const noexcept { + return Mu::to_string_opt(g_mime_part_get_content_description(self())); + } + + /** + * Gets the value of the Content-Id for this mime part + * if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option<std::string> content_id() const noexcept { + return Mu::to_string_opt(g_mime_part_get_content_id(self())); + } + + /** + * Gets the value of the Content-Md5 header for this mime part + * if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option<std::string> content_md5() const noexcept { + return Mu::to_string_opt(g_mime_part_get_content_md5(self())); + + } + + /** + * Verify the content md5 for the specified mime part. Returns false if + * the mime part does not contain a Content-MD5. + * + * @return true or false + */ + bool verify_content_md5() const noexcept { + return g_mime_part_verify_content_md5(self()); + } + + /** + * Gets the value of the Content-Location for this mime part if it + * exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option<std::string> content_location() const noexcept { + return Mu::to_string_opt(g_mime_part_get_content_location(self())); + } + + + MimeDataWrapper content() const noexcept { + return MimeDataWrapper{g_mime_part_get_content(self())}; + } + + /** + * Gets the filename for this mime part if it exists, or nullopt + * otherwise. + * + * @return string or nullopt + */ + Option<std::string> filename() const noexcept { + return Mu::to_string_opt(g_mime_part_get_filename(self())); + } + + /** + * Size of content, in bytes + * + * @return size + */ + size_t size() const noexcept; + + /** + * Get as UTF-8 string + * + * @return a string, or NULL. + */ + Option<std::string> to_string() const noexcept; + + + /** + * Write part to a file + * + * @param path path to file + * @param overwrite if true, overwrite existing file, if it bqexists + * + * @return size of the wrtten file, or an error. + */ + Result<size_t> to_file(const std::string& path, bool overwrite) + const noexcept; + + + /** + * Types of Content Encoding. + * + */ + enum struct ContentEncoding { + Default = GMIME_CONTENT_ENCODING_DEFAULT, + SevenBit = GMIME_CONTENT_ENCODING_7BIT, + EightBit = GMIME_CONTENT_ENCODING_8BIT, + Binary = GMIME_CONTENT_ENCODING_BINARY, + Base64 = GMIME_CONTENT_ENCODING_BASE64, + QuotedPrintable = GMIME_CONTENT_ENCODING_QUOTEDPRINTABLE, + UuEncode = GMIME_CONTENT_ENCODING_UUENCODE + }; + + /** + * Gets the content encoding of the mime part. + * + * @return the content encoding + */ + ContentEncoding content_encoding() const noexcept { + const auto enc{g_mime_part_get_content_encoding(self())}; + g_return_val_if_fail(enc <= GMIME_CONTENT_ENCODING_UUENCODE, + ContentEncoding::Default); + return static_cast<ContentEncoding>(enc); + } + + + /** + * Types of OpenPGP data + * + */ + enum struct OpenPGPData { + None = GMIME_OPENPGP_DATA_NONE, + Encrypted = GMIME_OPENPGP_DATA_ENCRYPTED, + Signed = GMIME_OPENPGP_DATA_SIGNED, + PublicKey = GMIME_OPENPGP_DATA_PUBLIC_KEY, + PrivateKey = GMIME_OPENPGP_DATA_PRIVATE_KEY, + }; + + /** + * Gets whether or not (and what type) of OpenPGP data is contained + * + * @return OpenGPGData + */ + OpenPGPData openpgp_data() const noexcept { + const auto data{g_mime_part_get_openpgp_data(self())}; + g_return_val_if_fail(data <= GMIME_OPENPGP_DATA_PRIVATE_KEY, + OpenPGPData::None); + return static_cast<OpenPGPData>(data); + } + +private: + GMimePart* self() const { + return reinterpret_cast<GMimePart*>(object()); + } +}; + + + +/** + * Thin wrapper around a GMimeMessagePart. + * + */ +class MimeMessagePart: public MimeObject { +public: + /** + * Construct a MimeMessagePart + * + * @param obj an Object of the right type + */ + MimeMessagePart(const Object& obj): MimeObject(obj) { + if (!is_message_part()) + throw std::runtime_error("not a mime-message-part"); + } + + /** + * Get the MimeMessage for this MimeMessagePart. + * + * @return the MimeMessage or Nothing + */ + Option<MimeMessage> get_message() const { + auto msg{g_mime_message_part_get_message(self())}; + if (msg) + return MimeMessage(Object(G_OBJECT(msg))); + else + return Nothing; + } +private: + GMimeMessagePart* self() const { + return reinterpret_cast<GMimeMessagePart*>(object()); + } + +}; + /** + * Thin wrapper around a GMimeApplicationPkcs7Mime + * + */ +class MimeApplicationPkcs7Mime: public MimePart { +public: + /** + * Construct a MimeApplicationPkcs7Mime + * + * @param obj an Object of the right type + */ + MimeApplicationPkcs7Mime(const Object& obj): MimePart(obj) { + if (!is_mime_application_pkcs7_mime()) + throw std::runtime_error("not a mime-application-pkcs7-mime"); + } + + enum struct SecureMimeType { + CompressedData = GMIME_SECURE_MIME_TYPE_COMPRESSED_DATA, + EnvelopedData = GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA, + SignedData = GMIME_SECURE_MIME_TYPE_SIGNED_DATA, + CertsOnly = GMIME_SECURE_MIME_TYPE_CERTS_ONLY, + Unknown = GMIME_SECURE_MIME_TYPE_UNKNOWN + }; + + SecureMimeType smime_type() const { + return static_cast<SecureMimeType>( + g_mime_application_pkcs7_mime_get_smime_type(self())); + } + +private: + GMimeApplicationPkcs7Mime* self() const { + return reinterpret_cast<GMimeApplicationPkcs7Mime*>(object()); + } +}; + + +/** + * Thin wrapper around a GMimeMultiPart + * + */ +class MimeMultipart: public MimeObject { +public: + /** + * Construct a MimeMultipart + * + * @param obj an Object of the right type + */ + MimeMultipart(const Object& obj): MimeObject(obj) { + if (!is_multipart()) + throw std::runtime_error("not a mime-multipart"); + } + + Option<MimePart> signed_content_part() const { + return part(GMIME_MULTIPART_SIGNED_CONTENT); + } + + Option<MimePart> signed_signature_part() const { + return part(GMIME_MULTIPART_SIGNED_SIGNATURE); + } + + Option<MimePart> encrypted_version_part() const { + return part(GMIME_MULTIPART_ENCRYPTED_VERSION); + } + + Option<MimePart> encrypted_content_part() const { + return part(GMIME_MULTIPART_ENCRYPTED_CONTENT); + } + + /** + * Recursively apply func to all parts + * + * @param func a function + */ + void for_each(const ForEachFunc& func) const noexcept; + +private: + + Option<MimePart> part(int index) const { + if (MimeObject mobj{g_mime_multipart_get_part(self(),index)}; !mobj) + return Nothing; + else + return mobj; + } + + GMimeMultipart* self() const { + return reinterpret_cast<GMimeMultipart*>(object()); + } +}; + + +/** + * Thin wrapper around a GMimeMultiPartEncrypted + * + */ +class MimeMultipartEncrypted: public MimeMultipart { +public: + /** + * Construct a MimeMultipartEncrypted + * + * @param obj an Object of the right type + */ + MimeMultipartEncrypted(const Object& obj): MimeMultipart(obj) { + if (!is_multipart_encrypted()) + throw std::runtime_error("not a mime-multipart-encrypted"); + } + + enum struct DecryptFlags { + None = GMIME_DECRYPT_NONE, + ExportSessionKey = GMIME_DECRYPT_EXPORT_SESSION_KEY, + NoVerify = GMIME_DECRYPT_NO_VERIFY, + EnableKeyserverLookups = GMIME_DECRYPT_ENABLE_KEYSERVER_LOOKUPS, + EnableOnlineCertificateChecks = GMIME_DECRYPT_ENABLE_ONLINE_CERTIFICATE_CHECKS + }; + + using Decrypted = std::pair<MimeObject, MimeDecryptResult>; + Result<Decrypted> decrypt(const MimeCryptoContext& ctx, + DecryptFlags flags=DecryptFlags::None, + const std::string& session_key = {}) const noexcept; + +private: + GMimeMultipartEncrypted* self() const { + return reinterpret_cast<GMimeMultipartEncrypted*>(object()); + } +}; + +MU_ENABLE_BITOPS(MimeMultipartEncrypted::DecryptFlags); + + +/** + * Thin wrapper around a GMimeMultiPartSigned + * + */ +class MimeMultipartSigned: public MimeMultipart { +public: + /** + * Construct a MimeMultipartSigned + * + * @param obj an Object of the right type + */ + MimeMultipartSigned(const Object& obj): MimeMultipart(obj) { + if (!is_multipart_signed()) + throw std::runtime_error("not a mime-multipart-signed"); + } + + enum struct VerifyFlags { + None = GMIME_VERIFY_NONE, + EnableKeyserverLookups = GMIME_VERIFY_ENABLE_KEYSERVER_LOOKUPS, + EnableOnlineCertificateChecks = GMIME_VERIFY_ENABLE_ONLINE_CERTIFICATE_CHECKS + }; + + // Result<std::vector<MimeSignature>> verify(VerifyFlags vflags=VerifyFlags::None) const noexcept; + + Result<std::vector<MimeSignature>> verify(const MimeCryptoContext& ctx, + VerifyFlags vflags=VerifyFlags::None) const noexcept; + +private: + GMimeMultipartSigned* self() const { + return reinterpret_cast<GMimeMultipartSigned*>(object()); + } +}; + + +MU_ENABLE_BITOPS(MimeMultipartSigned::VerifyFlags); + +} // namespace Mu + + +#endif /* MU_MIME_OBJECT_HH__ */ diff --git a/lib/message/mu-priority.cc b/lib/message/mu-priority.cc new file mode 100644 index 0000000..9b57cea --- /dev/null +++ b/lib/message/mu-priority.cc @@ -0,0 +1,76 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-priority.hh" + +using namespace Mu; + +std::string +Mu::to_string(Priority prio) +{ + return std::string{priority_name(prio)}; +} + +/* + * tests... also build as runtime-tests, so we can get coverage info + */ +#ifdef BUILD_TESTS +#include <glib.h> +#define static_assert g_assert_true +#endif /*BUILD_TESTS*/ + +[[maybe_unused]] static void +test_priority_to_char() +{ + static_assert(to_char(Priority::Low) == 'l'); + static_assert(to_char(Priority::Normal) == 'n'); + static_assert(to_char(Priority::High) == 'h'); +} + +[[maybe_unused]] static void +test_priority_from_char() +{ + static_assert(priority_from_char('l') == Priority::Low); + static_assert(priority_from_char('n') == Priority::Normal); + static_assert(priority_from_char('h') == Priority::High); + static_assert(priority_from_char('x') == Priority::Normal); +} + +[[maybe_unused]] static void +test_priority_name() +{ + static_assert(priority_name(Priority::Low) == "low"); + static_assert(priority_name(Priority::Normal) == "normal"); + static_assert(priority_name(Priority::High) == "high"); +} + + +#ifdef BUILD_TESTS +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/message/priority/to-char", test_priority_to_char); + g_test_add_func("/message/priority/from-char", test_priority_from_char); + g_test_add_func("/message/priority/name", test_priority_name); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-priority.hh b/lib/message/mu-priority.hh new file mode 100644 index 0000000..af76ece --- /dev/null +++ b/lib/message/mu-priority.hh @@ -0,0 +1,129 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_PRIORITY_HH__ +#define MU_PRIORITY_HH__ + +#include <array> +#include <string> +#include <string_view> +#include "mu-fields.hh" + +namespace Mu { +/** + * Message priorities + * + */ + +/** + * The priority ids + * + */ +enum struct Priority : char { + Low = 'l', /**< Low priority */ + Normal = 'n', /**< Normal priority */ + High = 'h', /**< High priority */ +}; + +/** + * Sequence of all message priorities. + */ +static constexpr std::array<Priority, 3> AllMessagePriorities = { + Priority::Low, Priority::Normal, Priority::High}; + +/** + * Get the char for some priority + * + * @param id an id + * + * @return the char + */ +constexpr char +to_char(Priority prio) +{ + return static_cast<char>(prio); +} + +/** + * Get the priority for some character; unknown onws + * become Normal. + * + * @param c some character + */ +constexpr Priority +priority_from_char(char c) +{ + switch (c) { + case 'l': + return Priority::Low; + case 'h': + return Priority::High; + case 'n': + default: + return Priority::Normal; + } +} + +/** + * Get the name for a given priority + * + * @return the name + */ +constexpr std::string_view +priority_name(Priority prio) +{ + switch (prio) { + case Priority::Low: + return "low"; + case Priority::High: + return "high"; + case Priority::Normal: + default: + return "normal"; + } +} + +/** + * Get the name for a given priority (backward compatibility) + * + * @return the name + */ +constexpr const char* +priority_name_c_str(Priority prio) +{ + switch (prio) { + case Priority::Low: return "low"; + case Priority::High: return "high"; + case Priority::Normal: + default: return "normal"; + } +} + +/** + * Get a the message priority as a string + * + * @param prio priority + * + * @return a string + */ +std::string to_string(Priority prio); + +} // namespace Mu + +#endif /*MU_PRIORITY_HH_*/ diff --git a/lib/message/test-mu-message.cc b/lib/message/test-mu-message.cc new file mode 100644 index 0000000..a96b44b --- /dev/null +++ b/lib/message/test-mu-message.cc @@ -0,0 +1,1068 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include "utils/mu-test-utils.hh" +#include "mu-message.hh" +#include "mu-mime-object.hh" +#include <glib.h> +#include <regex> + +using namespace Mu; + +/* + * test message 1 + */ + +static void +test_message_mailing_list() +{ + constexpr const char *test_message_1 = +R"(Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST) +Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: anon@example.com +To: sqlite-dev@sqlite.org +Mime-Version: 1.0 (Apple Message framework v926) +Date: Mon, 4 Aug 2008 11:40:49 +0200 +X-Mailer: Apple Mail (2.926) +Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: sqlite-dev-bounces@sqlite.org +Content-Length: 639 + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; +)"; + auto message{Message::make_from_text( + test_message_1, + "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S")}; + g_assert_true(!!message); + assert_equal(message->path(), + "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S"); + g_assert_true(message->maildir().empty()); + + g_assert_true(message->bcc().empty()); + + g_assert_true(!message->body_html()); + assert_equal(message->body_text().value_or(""), +R"(Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; +)"); + g_assert_true(message->cc().empty()); + g_assert_cmpuint(message->date(), ==, 1217842849); + g_assert_true(message->flags() == (Flags::MailingList | Flags::Seen)); + + const auto from{message->from()}; + g_assert_cmpuint(from.size(),==,1); + assert_equal(from.at(0).name, ""); + assert_equal(from.at(0).email, "anon@example.com"); + + assert_equal(message->mailing_list(), "sqlite-dev.sqlite.org"); + assert_equal(message->message_id(), + "83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net"); + + g_assert_true(message->priority() == Priority::Low); + g_assert_cmpuint(message->size(),==,::strlen(test_message_1)); + + /* text-based message use time({}) as their changed-time */ + g_assert_cmpuint(::time({}) - message->changed(), >=, 0); + g_assert_cmpuint(::time({}) - message->changed(), <=, 2); + + g_assert_true(message->references().empty()); + + assert_equal(message->subject(), + "[sqlite-dev] VM optimization inside sqlite3VdbeExec"); + + const auto to{message->to()}; + g_assert_cmpuint(to.size(),==,1); + assert_equal(to.at(0).name, ""); + assert_equal(to.at(0).email, "sqlite-dev@sqlite.org"); + + assert_equal(message->header("X-Mailer").value_or(""), "Apple Mail (2.926)"); + + auto all_contacts{message->all_contacts()}; + g_assert_cmpuint(all_contacts.size(), ==, 4); + seq_sort(all_contacts, [](auto&& c1, auto&& c2){return c1.email < c2.email; }); + assert_equal(all_contacts[0].email, "anon@example.com"); + assert_equal(all_contacts[1].email, "sqlite-dev-bounces@sqlite.org"); + assert_equal(all_contacts[2].email, "sqlite-dev@sqlite.org"); + assert_equal(all_contacts[3].email, "sqlite-dev@sqlite.org"); +} + + +static void +test_message_attachments(void) +{ + constexpr const char* msg_text = +R"(Return-Path: <foo@example.com> +Received: from pop.gmail.com [256.85.129.309] + by evergrey with POP3 (fetchmail-6.4.29) + for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET) +Sender: "Foo, Example" <foo@example.com> +User-agent: mu4e 1.7.11; emacs 29.0.50 +From: "Foo Example" <foo@example.com> +To: bar@example.com +Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?= +Date: Thu, 24 Mar 2022 20:04:39 +0200 +Organization: ACME Inc. +Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain + +Hello, +--=-=-= +Content-Type: image/jpeg +Content-Disposition: attachment; filename=file-01.bin +Content-Transfer-Encoding: base64 + +AAECAw== +--=-=-= +Content-Type: audio/ogg +Content-Disposition: inline; filename=/tmp/file-02.bin +Content-Transfer-Encoding: base64 + +BAUGBw== +--=-=-= +Content-Type: message/rfc822 +Content-Disposition: attachment; + filename="message.eml" + +From: "Fnorb" <fnorb@example.com> +To: Bob <bob@example.com> +Subject: news for you +Date: Mon, 28 Mar 2022 22:53:26 +0300 + +Attached message! + +--=-=-= +Content-Type: text/plain + +World! +--=-=-=-- +)"; + + auto message{Message::make_from_text(msg_text)}; + g_assert_true(!!message); + g_assert_true(message->has_mime_message()); + g_assert_true(message->path().empty()); + + g_assert_true(message->bcc().empty()); + g_assert_true(!message->body_html()); + assert_equal(message->body_text().value_or(""), R"(Hello,World!)"); + + g_assert_true(message->cc().empty()); + g_assert_cmpuint(message->date(), ==, 1648145079); + /* no Flags::Unread since it's a message without path */ + g_assert_true(message->flags() == (Flags::HasAttachment)); + + const auto from{message->from()}; + g_assert_cmpuint(from.size(),==,1); + assert_equal(from.at(0).name, "Foo Example"); + assert_equal(from.at(0).email, "foo@example.com"); + + // problem case: https://github.com/djcb/mu/issues/2232o + assert_equal(message->message_id(), + "3144HPOJ0VC77.3H1XTAG2AMTLH@\"@WILSONB.COM"); + + g_assert_true(message->path().empty()); + g_assert_true(message->priority() == Priority::Normal); + g_assert_cmpuint(message->size(),==,::strlen(msg_text)); + + /* text-based message use time({}) as their changed-time */ + g_assert_cmpuint(::time({}) - message->changed(), >=, 0); + g_assert_cmpuint(::time({}) - message->changed(), <=, 2); + + assert_equal(message->subject(), "ättächmeñts"); + + const auto cache_path{message->cache_path()}; + g_assert_true(!!cache_path); + + g_assert_cmpuint(message->parts().size(),==,5); + { + auto&& part{message->parts().at(0)}; + g_assert_false(!!part.raw_filename()); + assert_equal(part.mime_type().value(), "text/plain"); + assert_equal(part.to_string().value(), "Hello,"); + } + { + auto&& part{message->parts().at(1)}; + assert_equal(part.raw_filename().value(), "file-01.bin"); + assert_equal(part.mime_type().value(), "image/jpeg"); + // file consists of 4 bytes 0...3 + g_assert_cmpuint(part.to_string()->at(0), ==, 0); + g_assert_cmpuint(part.to_string()->at(1), ==, 1); + g_assert_cmpuint(part.to_string()->at(2), ==, 2); + g_assert_cmpuint(part.to_string()->at(3), ==, 3); + } + { + auto&& part{message->parts().at(2)}; + assert_equal(part.raw_filename().value(), "/tmp/file-02.bin"); + assert_equal(part.cooked_filename().value(), "tmp-file-02.bin"); + assert_equal(part.mime_type().value(), "audio/ogg"); + // file consistso of 4 bytes 4..7 + assert_equal(part.to_string().value(), "\004\005\006\007"); + const auto fpath{*cache_path + part.cooked_filename().value()}; + const auto res = part.to_file(fpath, true); + + g_assert_cmpuint(*res,==,4); + g_assert_cmpuint(::access(fpath.c_str(), R_OK), ==, 0); + } + + { + auto&& part{message->parts().at(3)}; + g_assert_true(part.mime_type() == "message/rfc822"); + } + + { + auto&& part{message->parts().at(4)}; + g_assert_false(!!part.raw_filename()); + g_assert_true(!!part.mime_type()); + assert_equal(part.mime_type().value(), "text/plain"); + assert_equal(part.to_string().value(), "World!"); + } +} + + +/* + * some test keys. + */ + +constexpr std::string_view pub_key = +R"(-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN +hQhc+A+0LU11IFRlc3QgKG11IHRlc3Rpbmcga2V5KSA8bXVAZGpjYnNvZnR3YXJl +Lm5sPoiUBBMWCgA8FiEE/HZRT+2bPjARz29Cw7FsU49t3vAFAmJW2jYCGwMFCwkI +BwIDIgIBBhUKCQgLAgQWAgMBAh4HAheAAAoJEMOxbFOPbd7wJ2kBAIGmUDWYEPtn +qYTwhZIdZtTa4KJ3UdtTqey9AnxJ9mzAAQDRJOoVppj5wW2xRhgYP+ysN2iBUYGE +MhahOcNgxodbCLg4BGJW2jYSCisGAQQBl1UBBQEBB0D4Sp+GTVre7Cx5a8D3SwLJ +/bRAVGDwqI7PL9B/cMmCTwMBCAeIeAQYFgoAIBYhBPx2UU/tmz4wEc9vQsOxbFOP +bd7wBQJiVto2AhsMAAoJEMOxbFOPbd7w1tYA+wdfYCcwOP0QoNZZz2Yk12YkDk2R +FsRrZZpb0GKC/a2VAP4qFceeSegcUCBTQaoeFE9vq9XiUVOO98QI8r9C8QwvBw== +=jM/g +-----END PGP PUBLIC KEY BLOCK----- +)"; + +constexpr std::string_view priv_key = // "test1234" +R"(-----BEGIN PGP PRIVATE KEY BLOCK----- + +lIYEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN +hQhc+A/+BwMCz6T2uBpk6a7/rXyE7C1bRbGjP6YSFcyRFz8VRV3Xlm7z6rdbdKZr +8R15AtLvXA4DOK5GiZRB2VbIxi8B9CtZ9qQx6YbQPkAmRzISGAjECrQtTXUgVGVz +dCAobXUgdGVzdGluZyBrZXkpIDxtdUBkamNic29mdHdhcmUubmw+iJQEExYKADwW +IQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbAwULCQgHAgMiAgEGFQoJCAsC +BBYCAwECHgcCF4AACgkQw7FsU49t3vAnaQEAgaZQNZgQ+2ephPCFkh1m1NrgondR +21Op7L0CfEn2bMABANEk6hWmmPnBbbFGGBg/7Kw3aIFRgYQyFqE5w2DGh1sInIsE +YlbaNhIKKwYBBAGXVQEFAQEHQPhKn4ZNWt7sLHlrwPdLAsn9tEBUYPCojs8v0H9w +yYJPAwEIB/4HAwI9MZDWcsoiJ/9oV5DRiAedeo3Ta/1M+aKfeNV36Ch1VGLwQF3E +V77qIrJlsT8CwOZHWUksUBENvG3ak3vd84awHHaHoTmoFwtISfvQrFK0iHgEGBYK +ACAWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbDAAKCRDDsWxTj23e8NbW +APsHX2AnMDj9EKDWWc9mJNdmJA5NkRbEa2WaW9Bigv2tlQD+KhXHnknoHFAgU0Gq +HhRPb6vV4lFTjvfECPK/QvEMLwc= +=w1Nc +-----END PGP PRIVATE KEY BLOCK----- +)"; + + +static void +test_message_signed(void) +{ + constexpr const char *msgtext = +R"(Return-Path: <diggler@gmail.com> +From: Mu Test <mu@djcbsoftware.nl> +To: Mu Test <mu@djcbsoftware.nl> +Subject: boo +Date: Wed, 13 Apr 2022 17:19:08 +0300 +Message-ID: <878rs9ysin.fsf@djcbsoftware.nl> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha512; protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain + +Sapperdeflap + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iIkEARYKADEWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbcLhMcbXVAZGpjYnNv +ZnR3YXJlLm5sAAoJEMOxbFOPbd7waIkA/jK1oY7OL8vrDoubNYxamy8HHmwtvO01 +Q46aYjxe0As6AP90bcAZ3dcn5RcTJaM0UhZssguawZ+tnriD3+5DPkMMCg== +=e32+ +-----END PGP SIGNATURE----- +--=-=-=-- +)"; + TempDir tempdir; + auto ctx{MimeCryptoContext::make_gpg(tempdir.path())}; + g_assert_true(!!ctx); + + auto stream{MimeStream::make_mem()}; + stream.write(pub_key.data(), pub_key.size()); + stream.reset(); + + auto imported = ctx->import_keys(stream); + g_assert_cmpuint(*imported, ==, 1); + + auto message{Message::make_from_text( + msgtext, + "/home/test/Maildir/inbox/cur/1649279777.107710_1.mindcrime:2,RS")}; + g_assert_true(!!message); + + g_assert_true(message->bcc().empty()); + assert_equal(message->body_text().value_or(""), "Sapperdeflap\n"); + g_assert_true(message->flags() == (Flags::Signed|Flags::Seen|Flags::Replied)); + + size_t n{}; + for (auto&& part: message->parts()) { + if (!part.is_signed()) + continue; + + const auto& mobj{part.mime_object()}; + if (!mobj.is_multipart_signed()) + continue; + + const auto mpart{MimeMultipartSigned(mobj)}; + const auto sigs{mpart.verify(*ctx)}; + if (!sigs) + g_warning("%s", sigs.error().what()); + + g_assert_true(!!sigs); + g_assert_cmpuint(sigs->size(), ==, 1); + ++n; + } + + g_assert_cmpuint(n, ==, 1); +} + + +static void +test_message_signed_encrypted(void) +{ + constexpr const char *msgtext = +R"(From: "Mu Test" <mu@djcbsoftware.nl> +To: mu@djcbsoftware.nl +Subject: encrypted and signed +Date: Wed, 13 Apr 2022 17:32:30 +0300 +Message-ID: <87lew9xddt.fsf@djcbsoftware.nl> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- + +hF4DeEerj6WhdZASAQdAKdZwmugAlQA8c06Q5iQw4rwSADgfEWBTWlI6tDw7hEAw +0qSSeeQbA802qjG5TesaDVbFoPp1gOESt67HkJBABj9niwZLnjbzVRXKFoPTYabu +1MBWAQkCEO6kS0N73XQeJ9+nDkUacRX6sSgVM0j+nRdCGcrCQ8MOfLd9KUUBxpXy +r/rIBMpZGOIpKJnoZ2x75VsQIp/ADHLe9zzXVe0tkahXJqvLo26w3gn4NSEIEDp6 +4T/zMZImqGrENaixNmRiRSAnwPkLt95qJGOIqYhuW3X6hMRZyU4zDNwkAvnK+2Fv +Wjd+EmiFzh5tvCmPOSj556YFMV7UpFWO9VznXX/T5+f4i+95Lsm9Uotv/SiNtNQG +DPU3wiL347SzmPFXckknjlzSzDL1XbdbHdmoJs0uNnbaZxRwhkuTYbLHdpBZrBgR +C0bdoCx44QVU8HaZ2x91h3GoM/0q5bqM/rvCauwbokiJgAUrznecNPY= +=Ado7 +-----END PGP MESSAGE----- +--=-=-=-- +)"; + TempDir tempdir; + auto ctx{MimeCryptoContext::make_gpg(tempdir.path())}; + g_assert_true(!!ctx); + + /// test1234 + // ctx->set_request_password([](const MimeCryptoContext& ctx, + // const std::string& user_id, + // const std::string& prompt, + // bool reprompt, + // MimeStream& response)->Result<void> { + // return Err(Error::Code::Internal, "boo"); + // //return Ok(); + // }); + + { + auto stream{MimeStream::make_mem()}; + stream.write(priv_key.data(), priv_key.size()); + stream.write(pub_key.data(), pub_key.size()); + stream.reset(); + + + g_assert_cmpint(ctx->import_keys(stream).value_or(-1),==,1); + } + + auto message{Message::make_from_text( + msgtext, + "/home/test/Maildir/inbox/cur/1649279888.107710_1.mindcrime:2,FS")}; + g_assert_true(!!message); + g_assert_true(message->flags() == (Flags::Encrypted|Flags::Seen|Flags::Flagged)); + + size_t n{}; + for (auto&& part: message->parts()) { + + if (!part.is_encrypted()) + continue; + + g_assert_false(!!part.content_description()); + g_assert_false(part.is_attachment()); + g_assert_cmpuint(part.size(),==,0); + + const auto& mobj{part.mime_object()}; + if (!mobj.is_multipart_encrypted()) + continue; + + /* FIXME: make this work without user having to + * type password */ + + // const auto mpart{MimeMultipartEncrypted(mobj)}; + // const auto decres = mpart.decrypt(*ctx); + // assert_valid_result(decres); + + ++n; + } + + g_assert_cmpuint(n, ==, 1); +} + + +static void +test_message_multipart_mixed_rfc822(void) +{ + constexpr const char *msgtext = +R"(Content-Type: multipart/mixed; + boundary="Multipart_Tue_Sep__2_15:42:35_2014-1" + +--Multipart_Tue_Sep__2_15:42:35_2014-1 +Content-Type: message/rfc822 +)"; + auto message{Message::make_from_text(msgtext)}; + g_assert_true(!!message); + + g_assert_true(message->cached_sexp().empty()); +} + + +static void +test_message_detect_attachment(void) +{ + constexpr const char *msgtext = +R"(From: "DUCK, Donald" <donald@example.com> +Date: Tue, 3 May 2022 10:26:26 +0300 +Message-ID: <SADKLAJCLKDJLAS-xheQjE__+hS-3tff=pTYpMUyGiJwNGF_DA@mail.gmail.com> +Subject: =?Windows-1252?Q?Purkuty=F6urakka?= +To: Hello <moika@example.com> +Content-Type: multipart/mixed; boundary="000000000000e687ed05de166d71" + +--000000000000e687ed05de166d71 +Content-Type: multipart/alternative; boundary="000000000000e687eb05de166d6f" + +--000000000000e687eb05de166d6f +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +fyi + +---------- Forwarded message --------- +From: Fooish Bar <foobar@example.com> +Date: Tue, 3 May 2022 at 08:59 +Subject: Ty=C3=B6t +To: "DUCK, Donald" <donald@example.com> + +Moi, + +-- + +--000000000000e687eb05de166d6f +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +abc + +--000000000000e687eb05de166d6f-- +--000000000000e687ed05de166d71 +Content-Type: application/pdf; + name="test1.pdf" +Content-Disposition: attachment; + filename="test2.pdf" +Content-Transfer-Encoding: base64 +Content-ID: <18088cfd4bc5517c6321> +X-Attachment-Id: 18088cfd4bc5517c6321 + +JVBERi0xLjcKJeLjz9MKNyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDEgMCBSIC9MYXN0 +TW9kaWZpZWQgKEQ6MjAyMjA1MDMwODU3MzYrMDMnMDAnKSAvUmVzb3VyY2VzIDIgMCBSIC9NZWRp +cmVmCjM1NjE4CiUlRU9GCg== +--000000000000e687ed05de166d71-- +)"; + auto message{Message::make_from_text(msgtext)}; + g_assert_true(!!message); + + g_assert_true(message->path().empty()); + + g_assert_true(message->bcc().empty()); + assert_equal(message->subject(), "Purkutyöurakka"); + assert_equal(message->body_html().value_or(""), "abc\n"); + assert_equal(message->body_text().value_or(""), + R"(fyi + +---------- Forwarded message --------- +From: Fooish Bar <foobar@example.com> +Date: Tue, 3 May 2022 at 08:59 +Subject: Työt +To: "DUCK, Donald" <donald@example.com> + +Moi, + +-- +)"); + g_assert_true(message->cc().empty()); + g_assert_cmpuint(message->date(), ==, 1651562786); + g_assert_true(message->flags() == (Flags::HasAttachment)); + + g_assert_cmpuint(message->parts().size(), ==, 3); + + for (auto&& part: message->parts()) + g_info("%s %s", + part.is_attachment() ? "yes" : "no", + part.mime_type().value_or("boo").c_str()); +} + + +static void +test_message_calendar(void) +{ + constexpr const char *msgtext = +R"(MIME-Version: 1.0 +From: William <william@example.com> +To: Billy <billy@example.com> +Date: Thu, 9 Jan 2014 11:09:34 +0100 +Subject: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 + (william@example.com) +Thread-Topic: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 + (william@example.com) +Thread-Index: Ac8NIuske7OtG01VRpukb/bHE7SVHg== +Message-ID: <001a11c3440066ee0b04ef86cea8@google.com> +Accept-Language: en-US +Content-Language: en-US +X-MS-Exchange-Organization-AuthAs: Anonymous +X-MS-Has-Attach: yes +Content-Type: multipart/mixed; + boundary="_004_001a11c3440066ee0b04ef86cea8googlecom_" + +--_004_001a11c3440066ee0b04ef86cea8googlecom_ +Content-Type: multipart/alternative; + boundary="_002_001a11c3440066ee0b04ef86cea8googlecom_" + +--_002_001a11c3440066ee0b04ef86cea8googlecom_ +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: base64 + +PGh0bWw+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0i +dGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29udGVu +dD0iTWljcm9zb2Z0IEV4Y2hhbmdlIFNlcnZlciI+DQo8IS0tIGNvbnZlcnRlZCBmcm9tIHJ0ZiAt +LT4NCjxzdHlsZT48IS0tIC5FbWFpbFF1b3RlIHsgbWFyZ2luLWxlZnQ6IDFwdDsgcGFkZGluZy1s +ZWZ0OiA0cHQ7IGJvcmRlci1sZWZ0OiAjODAwMDAwIDJweCBzb2xpZDsgfSAtLT48L3N0eWxlPg0K +PC9oZWFkPg0KPGJvZHk+DQo8Zm9udCBmYWNlPSJUaW1lcyBOZXcgUm9tYW4iIHNpemU9IjMiPjxh +IG5hbWU9IkJNX0JFR0lOIj48L2E+DQo8dGFibGUgYm9yZGVyPSIxIiB3aWR0aD0iNzM0IiBzdHls +ZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTsgbWFyZ2luLWxlZnQ6 +IDJwdDsgIj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIxIj48YSBocmVmPSJodHRwczovL3d3dy5n +b29nbGUuY29tL2NhbGVuZGFyL2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxs +Ym1VeU0ySnZNbWsyYjNOeU56ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0 +b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016 +QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh +b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpkZXRh +aWxzIMK7PC91PjwvZm9udD48L2E+PGJyPg0KDQo8ZGl2IHN0eWxlPSJtYXJnaW4tYm90dG9tOiAx +NHB0OyAiPjxmb250IGZhY2U9IkFyaWFsLCBzYW5zLXNlcmlmIiBzaXplPSIyIiBjb2xvcj0iIzIy +MjIyMiI+PGI+SEVMTE8sPC9iPjwvZm9udD48L2Rpdj4NCjxkaXY+PGZvbnQgc2l6ZT0iMSIgY29s +b3I9IiMyMjIyMjIiPjxicj4NCg0KSSBBTSBERVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUg +U0lTVEVSIElTIEdMT1JJQSwgT1VSIEZBVEhFUiBPV05TIEEgTElNSVRFRCBPRiBDT0NPQSBBTkQg +R09MRCBCVVNJTkVTUyBJTiBSRVBVQkxJUVVFIERVIENPTkdPLiBBRlRFUiBISVMgVFJJUCBUTyBD +T1RFIERJVk9JUkUgVE8gTkVHT1RJQVRFIE9OIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdB +TlRFRCBUTyBJTlZFU1QgSU4gQUJST0FELiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0eWxlPSJtYXJn +aW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9IjMiPk9ORSBX +RUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURKQU4gSEUgSEFEIEEgTU9UT1Ig +QUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBNT1RIRVIgRElFRCBJTlNUQU5UTFkg +QlVUIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERBWVMgSU4gQSBQUklWQVRFIEhPU1BJVEFM +IElOIE9VUiBDT1VOVFJZLg0KSVQgV0FTIExJS0UgT1VSIEZBVEhFUiBLTkVXIEhFIFdBUyBHT0lO +RyBUTyBESUUgTUFZIEhJUyBHRU5UTEUgU09VTCBSRVNUIElOIFBSRUZFQ1QgUEVBQ0UuIDwvZm9u +dD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0b206IDE0 +cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+SEUgRElTQ0xPU0VEIFRPIE1FIEFTIFRIRSBPTkxZIFNPTiBU +SEFUIEhFIERFUE9TSVRFRCBUSEUgU1VNIE9GIChVU0QgJCAxMCw1MDAsMDAwKSBJTlRPIEEgQkFO +SyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGT1IgSElTIENPQ09BIEFORCBH +T0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4NCkFCUk9BRC5XRSBBUkUgU09M +SUNJVElORyBGT1IgWU9VUiBIRUxQIFRPIFRSQU5TRkVSIFRISVMgTU9ORVkgSU5UTyBZT1VSIEFD +Q09VTlQgSU4gWU9VUiBDT1VOVFJZIEZPUiBPVVIgSU5WRVNUTUVOVC4gPC9mb250PjwvZGl2Pg0K +PGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9u +dCBzaXplPSIzIj5QTEVBU0UgRk9SIFNFQ1VSSVRZIFJFQVNPTlMsSSBBRFZJQ0UgWU9VIFJFUExZ +IFVTIFRIUk9VR0ggT1VSIFBSSVZBVEUgRU1BSUw6IDxhIGhyZWY9Im1haWx0bzp3aWxsaWFtc2Rl +c21vbmQxMDdAeWFob28uY29tLnZuIj48Zm9udCBjb2xvcj0iIzAwMDBGRiI+PHU+d2lsbGlhbXNk +ZXNtb25kMTA3QHlhaG9vLmNvbS52bjwvdT48L2ZvbnQ+PC9hPg0KRk9SIE1PUkUgREVUQUlMUy4g +PC9mb250PjwvZGl2Pg0KPGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRv +bTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5SRUdBUkRTLiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0 +eWxlPSJtYXJnaW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9 +IjMiPkRFU01PTkQgL0dMT1JJQSBXSUxMSUFNUy48L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp +emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp +emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp +emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp +emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8dGFibGUgYm9yZGVy +PSIxIiB3aWR0aD0iNzM0IiBzdHlsZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpj +b2xsYXBzZTsgbWFyZ2luLWxlZnQ6IDJwdDsgIj4NCjxjb2wgd2lkdGg9IjM2NSI+DQo8Y29sIHdp +ZHRoPSIzNjkiPg0KPHRyPg0KPHRkPjxmb250IHNpemU9IjMiPjxpPldoZW48L2k+PC9mb250Pjwv +dGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIj +MjIyMjIyIj5UaHUgOSBKYW4gMjAxNCAwODozMCDigJMgMDk6MzAgPGZvbnQgY29sb3I9IiM4ODg4 +ODgiPlNhbyBQYXVsbzwvZm9udD48L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQ+PGZvbnQg +c2l6ZT0iMyI+PGk+Q2FsZW5kYXI8L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJp +YWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj53aWxsaWFtc19kMjBAZ2xv +Ym9tYWlsLmNvbTwvZm9udD48L3RkPg0KPC90cj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIzIj48 +aT5XaG88L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYi +IHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj4oR3Vlc3QgbGlzdCBoYXMgYmVlbiBoaWRkZW4gYXQg +b3JnYW5pc2VyJ3MgcmVxdWVzdCk8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3RhYmxlPg0KPGRpdiBz +dHlsZT0ibWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIxIiBjb2xvcj0iIzg4ODg4 +OCI+R29pbmc/Jm5ic3A7Jm5ic3A7IDxhIGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2Fs +ZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BPTkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1t +azJiM055TnpkbmNHOGdaR3BqWWtCa2FtTmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0xJmFtcDt0 +b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016 +QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh +b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5ZZXM8L2I+ +PC91PjwvZm9udD48L2E+PGZvbnQgY29sb3I9IiMyMjIyMjIiPjxiPg0KLSA8L2I+PC9mb250Pjxh +IGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BP +TkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1tazJiM055TnpkbmNHOGdaR3BqWWtCa2Ft +TmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0zJmFtcDt0b2s9TWpZamQybHNiR2xoYlhOZlpESXdR +R2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRqTm1ZMVlU +VXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxm +b250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5NYXliZTwvYj48L3U+PC9mb250PjwvYT48Zm9udCBj +b2xvcj0iIzIyMjIyMiI+PGI+DQotIDwvYj48L2ZvbnQ+PGEgaHJlZj0iaHR0cHM6Ly93d3cuZ29v +Z2xlLmNvbS9jYWxlbmRhci9ldmVudD9hY3Rpb249UkVTUE9ORCZhbXA7ZWlkPWMzTnpjV1F4Y0Rs +bGJtVXlNMkp2TW1rMmIzTnlOemRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZhbXA7 +cnN0PTImYW1wO3Rvaz1NallqZDJsc2JHbGhiWE5mWkRJd1FHZHNiMkp2YldGcGJDNWpiMjFqTXpj +MllUaGtZbUZrTXpBMlpEVXdOV1UyWW1ZeE5qZGpObVkxWVRVeE5tSmpNakU1TjJZMyZhbXA7Y3R6 +PUFtZXJpY2EvU2FvX1BhdWxvJmFtcDtobD1lbl9HQiI+PGZvbnQgY29sb3I9IiMyMjAwQ0MiPjx1 +PjxiPk5vPC9iPjwvdT48L2ZvbnQ+PC9hPjxmb250IGNvbG9yPSIjMjIyMjIyIj4mbmJzcDsmbmJz +cDsmbmJzcDsNCjwvZm9udD48YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFy +L2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxsYm1VeU0ySnZNbWsyYjNOeU56 +ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0b2s9TWpZamQybHNiR2xoYlhO +ZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRq +Tm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5f +R0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpvcHRpb25zIMK7PC91PjwvZm9udD48 +L2E+PC9mb250PjwvZGl2Pg0KPC9mb250PjwvdGQ+DQo8L3RyPg0KPHRyPg0KPHRkIHN0eWxlPSJi +YWNrZ3JvdW5kLWNvbG9yOiAjRjZGNkY2OyAiPjxmb250IHNpemU9IjMiPkludml0YXRpb24gZnJv +bSA8YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+PGZvbnQgY29sb3I9 +IiMwMDAwRkYiPjx1Pkdvb2dsZSBDYWxlbmRhcjwvdT48L2ZvbnQ+PC9hPg0KPGRpdiBzdHlsZT0i +bWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5Z +b3UgYXJlIHJlY2VpdmluZyB0aGlzIGNvdXJ0ZXN5IGVtYWlsIGF0IHRoZSBhY2NvdW50IGRqY2JA +ZGpjYnNvZnR3YXJlLm5sIGJlY2F1c2UgeW91IGFyZSBhbiBhdHRlbmRlZSBvZiB0aGlzIGV2ZW50 +LjwvZm9udD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0 +b206IDE0cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+VG8gc3RvcCByZWNlaXZpbmcgZnV0dXJlIG5vdGlm +aWNhdGlvbnMgZm9yIHRoaXMgZXZlbnQsIGRlY2xpbmUgdGhpcyBldmVudC4gQWx0ZXJuYXRpdmVs +eSwgeW91IGNhbiBzaWduIHVwIGZvciBhIEdvb2dsZSBhY2NvdW50IGF0DQo8YSBocmVmPSJodHRw +czovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9jYWxl +bmRhci88L2E+IGFuZCBjb250cm9sIHlvdXIgbm90aWZpY2F0aW9uIHNldHRpbmdzIGZvciB5b3Vy +IGVudGlyZSBjYWxlbmRhci48L2ZvbnQ+PC9kaXY+DQo8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3Rh +YmxlPg0KPC9mb250Pg0KPC9ib2R5Pg0KPC9odG1sPg0K + +--_002_001a11c3440066ee0b04ef86cea8googlecom_ +Content-Type: text/calendar; charset="UTF-8"; method=REQUEST +Content-Transfer-Encoding: 7bit + +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +DTSTART:20140109T103000Z +DTEND:20140109T113000Z +DTSTAMP:20140109T100934Z +ORGANIZER;CN=William:mailto:william@example.com +UID:sssqd1p9ene23bo2i6osr77gpo@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=billy@example.com;X-NUM-GUESTS=0:mailto:billy@example.com +CREATED:20140109T100932Z +DESCRIPTION:\nI AM DESMOND WILLIAMS AND MY LITTLE SISTER IS GLORIA\, OUR FA + THER OWNS A LIMITED OF COCOA AND GOLD BUSINESS IN REPUBLIQUE DU CONGO. AFTE + R HIS TRIP TO COTE DIVOIRE TO NEGOTIATE ON COCOA AND GOLD BUSINESS HE WANTE + D TO INVEST IN ABROAD. \n\nONE WEEK HE CAME BACK FROM HIS TRIP TO ABIDJAN H + E HAD A MOTOR ACCIDENT WITH OUR MOTHER WHICH OUR MOTHER DIED INSTANTLY BUT + OUR FATHER DIED AFTER FIVE DAYS IN A PRIVATE HOSPITAL IN OUR COUNTRY. IT WA + S LIKE OUR FATHER KNEW HE WAS GOING TO DIE MAY HIS GENTLE SOUL REST IN PREF + ECT PEACE. \n\nHE DISCLOSED TO ME AS THE ONLY SON THAT HE DEPOSITED THE SUM + OF (USD $ 10\,500\,000) INTO A BANK IN ABIDJAN THAT THE MONEY WAS MEANT FO + R HIS COCOA AND GOLD BUSINESS HE WANTED TO ESTABLISH IN ABROAD.WE ARE SOLIC + ITING FOR YOUR HELP TO TRANSFER THIS MONEY INTO YOUR ACCOUNT IN YOUR COUNTR + Y FOR OUR INVESTMENT. \n\nPLEASE FOR SECURITY REASONS\,I ADVICE YOU REPLY U + S THROUGH OUR PRIVATE EMAIL FOR MORE DETAI + LS. \n\nREGARDS. \n\nDESMOND /GLORIA WILLIAMS.\nView your event at http://w + ww.google.com/calendar/event?action=VIEW&eid=c3NzcWQxcDllbmUyM2JvMmk2b3NyNz + dncG8gZGpjYkBkamNic29mdHdhcmUubmw&tok=MjYjd2lsbGlhbXNfZDIwQGdsb2JvbWFpbC5jb + 21jMzc2YThkYmFkMzA2ZDUwNWU2YmYxNjdjNmY1YTUxNmJjMjE5N2Y3&ctz=America/Sao_Pau + lo&hl=en_GB. +LAST-MODIFIED:20140109T100932Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:HELLO\, +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR + +--_002_001a11c3440066ee0b04ef86cea8googlecom_-- + +--_004_001a11c3440066ee0b04ef86cea8googlecom_ +Content-Type: application/ics; name="invite.ics" +Content-Description: invite.ics +Content-Disposition: attachment; filename="invite.ics"; size=2029; + creation-date="Thu, 09 Jan 2014 10:09:44 GMT"; + modification-date="Thu, 09 Jan 2014 10:09:44 GMT" +Content-Transfer-Encoding: base64 + +QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw +LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT +VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMTA5VDEwMzAwMFoNCkRURU5EOjIwMTQwMTA5 +VDExMzAwMFoNCkRUU1RBTVA6MjAxNDAxMDlUMTAwOTM0Wg0KT1JHQU5JWkVSO0NOPVdpbGxpYW1z +IFdpbGxpYW1zOm1haWx0bzp3aWxsaWFtc19kMjBAZ2xvYm9tYWlsLmNvbQ0KVUlEOnNzc3FkMXA5 +ZW5lMjNibzJpNm9zcjc3Z3BvQGdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM +O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7 +Q049ZGpjYkBkamNic29mdHdhcmUubmw7WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmRqY2JAZGpjYnNv +ZnR3YXJlLm5sDQpDUkVBVEVEOjIwMTQwMTA5VDEwMDkzMloNCkRFU0NSSVBUSU9OOlxuSSBBTSBE +RVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUgU0lTVEVSIElTIEdMT1JJQVwsIE9VUiBGQQ0K +IFRIRVIgT1dOUyBBIExJTUlURUQgT0YgQ09DT0EgQU5EIEdPTEQgQlVTSU5FU1MgSU4gUkVQVUJM +SVFVRSBEVSBDT05HTy4gQUZURQ0KIFIgSElTIFRSSVAgVE8gQ09URSBESVZPSVJFIFRPIE5FR09U +SUFURSBPTiBDT0NPQSBBTkQgR09MRCBCVVNJTkVTUyBIRSBXQU5URQ0KIEQgVE8gSU5WRVNUIElO +IEFCUk9BRC4gXG5cbk9ORSBXRUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURK +QU4gSA0KIEUgSEFEIEEgTU9UT1IgQUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBN +T1RIRVIgRElFRCBJTlNUQU5UTFkgQlVUIA0KIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERB +WVMgSU4gQSBQUklWQVRFIEhPU1BJVEFMIElOIE9VUiBDT1VOVFJZLiBJVCBXQQ0KIFMgTElLRSBP +VVIgRkFUSEVSIEtORVcgSEUgV0FTIEdPSU5HIFRPIERJRSBNQVkgSElTIEdFTlRMRSBTT1VMIFJF +U1QgSU4gUFJFRg0KIEVDVCBQRUFDRS4gXG5cbkhFIERJU0NMT1NFRCBUTyBNRSBBUyBUSEUgT05M +WSBTT04gVEhBVCBIRSBERVBPU0lURUQgVEhFIFNVTQ0KICBPRiAoVVNEICQgMTBcLDUwMFwsMDAw +KSBJTlRPIEEgQkFOSyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGTw0KIFIg +SElTIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4gQUJS +T0FELldFIEFSRSBTT0xJQw0KIElUSU5HIEZPUiBZT1VSIEhFTFAgVE8gVFJBTlNGRVIgVEhJUyBN +T05FWSBJTlRPIFlPVVIgQUNDT1VOVCBJTiBZT1VSIENPVU5UUg0KIFkgRk9SIE9VUiBJTlZFU1RN +RU5ULiBcblxuUExFQVNFIEZPUiBTRUNVUklUWSBSRUFTT05TXCxJIEFEVklDRSBZT1UgUkVQTFkg +VQ0KIFMgVEhST1VHSCBPVVIgUFJJVkFURSBFTUFJTDogd2lsbGlhbXNkZXNtb25kMTA3QHlhaG9v +LmNvbS52biBGT1IgTU9SRSBERVRBSQ0KIExTLiBcblxuUkVHQVJEUy4gXG5cbkRFU01PTkQgL0dM +T1JJQSBXSUxMSUFNUy5cblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vdw0KIHd3Lmdvb2dsZS5j +b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPWMzTnpjV1F4Y0RsbGJtVXlNMkp2TW1r +MmIzTnlOeg0KIGRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZ0b2s9TWpZamQybHNi +R2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYg0KIDIxak16YzJZVGhrWW1Ga016QTJaRFV3TldV +MlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmY3R6PUFtZXJpY2EvU2FvX1BhdQ0KIGxvJmhs +PWVuX0dCLg0KTEFTVC1NT0RJRklFRDoyMDE0MDEwOVQxMDA5MzJaDQpMT0NBVElPTjoNClNFUVVF +TkNFOjANClNUQVRVUzpDT05GSVJNRUQNClNVTU1BUlk6SEVMTE9cLA0KVFJBTlNQOk9QQVFVRQ0K +RU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg0K + +--_004_001a11c3440066ee0b04ef86cea8googlecom_-- + +)"; + auto message{Message::make_from_text( + msgtext, + "/home/test/Maildir/inbox/cur/162342449279256.107710_1.evergrey:2,PSp")}; + g_assert_true(!!message); + assert_equal(message->subject(), + "Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com)"); + g_assert_true(message->flags() == (Flags::Passed|Flags::Seen| + Flags::HasAttachment|Flags::Calendar)); +} + + +static void +test_message_references() +{ + constexpr auto msgtext = +R"(Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=utf-8 +References: <YuvYh1JbE3v+abd5@kili> + <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> + <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid> +To: "Robin Murphy" <robin.murphy@arm.com> +Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com> +From: "Dan Carpenter" <dan.carpenter@oracle.com> +Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs +List-Id: <kernel-janitors.vger.kernel.org> +Date: Fri, 5 Aug 2022 09:37:02 +0300 +In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> +Precedence: bulk +Message-Id: <20220805063702.GH3438@kadam> + +On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote: +> On 04/08/2022 3:32 pm, Dan Carpenter wrote: +> > There are two issues here: +)"; + auto message{Message::make_from_text( + msgtext, + "/home/test/Maildir/inbox/cur/162342449279256.88888_1.evergrey:2,S")}; + g_assert_true(!!message); + assert_equal(message->subject(), + "Re: [PATCH] iommu/omap: fix buffer overflow in debugfs"); + g_assert_true(message->priority() == Priority::Low); + + /* + * "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com" is seen both in + * references and in-reply-to; in the de-duplication, the first one wins. + */ + std::vector<std::string> expected_refs = { + "YuvYh1JbE3v+abd5@kili", + "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com", + /* protonmail.internalid is fake and removed */ + // "T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_" + // "xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid" + }; + + assert_equal_seq_str(expected_refs, message->references()); +} + + +static void +test_message_outlook_body() +{ + constexpr auto msgtext = +R"x(Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by + vu-ex1.activedir.vu.lt (172.16.159.218) with Microsoft SMTP Server + (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1118.9 + via Mailbox Transport; Fri, 27 May 2022 11:40:05 +0300 +Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by + vu-ex2.activedir.vu.lt (172.16.159.219) with Microsoft SMTP Server + (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id + 15.2.1118.9; Fri, 27 May 2022 11:40:05 +0300 +Received: from vu-ex2.activedir.vu.lt ([172.16.159.219]) by + vu-ex2.activedir.vu.lt ([172.16.159.219]) with mapi id 15.02.1118.009; Fri, + 27 May 2022 11:40:05 +0300 +From: =?windows-1257?Q?XXXXXXXXXX= <XXXXXXXXXX> +To: <XXXXXXXXXX@XXXXXXXXXX.com> +Subject: =?windows-1257?Q?Pra=F0ymas?= +Thread-Topic: =?windows-1257?Q?Pra=F0ymas?= +Thread-Index: AQHYcaRi3ejPSLxkl0uTFDto7z2OcA== +Date: Fri, 27 May 2022 11:40:05 +0300 +Message-ID: <5c2cd378af634e929a6cc69da1e66b9d@XX.vu.lt> +Accept-Language: en-US, lt-LT +Content-Language: en-US +X-MS-Has-Attach: +Content-Type: text/html; charset="windows-1257" +Content-Transfer-Encoding: quoted-printable +MIME-Version: 1.0 +X-TUID: 1vFQ9RPwwg/u + +<html> +<head> +<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dwindows-1= +257"> +<style type=3D"text/css" style=3D"display:none;"><!-- P {margin-top:0;margi= +n-bottom:0;} --></style> +</head> +<body dir=3D"ltr"> +<div id=3D"divtagdefaultwrapper" style=3D"font-size:12pt;color:#000000;font= +-family:Calibri,Helvetica,sans-serif;" dir=3D"ltr"> +<p>Laba diena visiems,</p> +<p>Trumpai.</p> +<p>D=EBl leidimo ar neleidimo ginti darb=E0: ed=EBstytojo paskyroje spaud= +=FEiate ikon=E0 "ra=F0to darbai", atidar=E6 susiraskite =E1ra=F0= +=E0 "tvirtinti / netvirtinti", pa=FEym=EBkite vien=E0 i=F0 j=F8.&= +nbsp;</p> +<p><br> +</p> +<p>=D0=E1 darb=E0 privalu atlikti, kad paskui nekilt=F8 problem=F8 studentu= +i =E1vedant =E1vertinim=E0.</p> +<p><br> +</p> +<p>Jei neleid=FEiate ginti darbo, pra=F0au informuoti mane ir komisijos sek= +retori=F8.  </p> +<p><br> +</p> +<p>Vis=E0 tolesn=E6 informacij=E0 atsi=F8siu artimiausiu metu (stengsiuosi = +=F0iandien vakare).</p> +<p><br> +</p> +<p>Pagarbiai.</p> +<p><br> +</p> +<p><br> +</p> +<div id=3D"Signature"> +<div id=3D"divtagdefaultwrapper" dir=3D"ltr" style=3D"font-family: Calibri,= + Helvetica, sans-serif, EmojiFont, "Apple Color Emoji", "Seg= +oe UI Emoji", NotoColorEmoji, "Segoe UI Symbol", "Andro= +id Emoji", EmojiSymbols;"> +<p style=3D"color:rgb(0,0,0); font-size:12pt"><br> +</p> +<p style=3D"color:rgb(0,0,0); font-size:12pt"><br> +</p> +<p style=3D"color:rgb(0,0,0); font-size:12pt"><br> +</p> +<p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt= +; background-color:rgb(255,255,255); color:rgb(0,111,201)"><br> +</span></p> +<p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt= +; background-color:rgb(255,255,255); color:rgb(0,111,201)">XXXXXXXXXX</span></p> +<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px"><= +/span></font></p> +<span style=3D"font-size:10pt; background-color:rgb(255,255,255); color:rgb= +(0,111,201); font-size:10pt"></span> +<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p> +<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p> +<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p> +<p style=3D""><br> +</p> +<p style=3D""><br> +</p> +</div> +</div> +</div> +</body> +</html> +)x"; + g_test_bug("2349"); + + auto message{Message::make_from_text( + msgtext, + "/home/test/Maildir/inbox/cur/162342449279256.77777_1.evergrey:2,S")}; + g_assert_true(!!message); + + assert_equal(message->subject(), "PraÅ¡ymas"); + g_assert_true(message->priority() == Priority::Normal); + + g_assert_false(!!message->body_text()); + g_assert_true(!!message->body_html()); + g_assert_cmpuint(message->body_html()->find("<p>Pagarbiai.</p>"), ==, 935); +} + + +static void +test_message_message_id() +{ + constexpr const auto msg1 = +R"(From: "Mu Test" <mu@djcbsoftware.nl> +To: mu@djcbsoftware.nl +Message-ID: <87lew9xddt.fsf@djcbsoftware.nl> + +abc +)"; + + constexpr const auto msg2 = +R"(From: "Mu Test" <mu@djcbsoftware.nl> +To: mu@djcbsoftware.nl + +abc +)"; + + constexpr const auto msg3 = +R"(From: "Mu Test" <mu@djcbsoftware.nl> +To: mu@djcbsoftware.nl +Message-ID: + +abc +)"; + + const auto m1{Message::make_from_text(msg1, "/foo/cur/m123:2,S")}; + assert_valid_result(m1); + + const auto m2{Message::make_from_text(msg2, "/foo/cur/m456:2,S")}; + assert_valid_result(m2); + const auto m3{Message::make_from_text(msg3, "/foo/cur/m789:2,S")}; + assert_valid_result(m3); + + assert_equal(m1->message_id(), "87lew9xddt.fsf@djcbsoftware.nl"); + + /* both with absent and empty message-id, generate "random" fake one, + * which must end in @mu.id */ + g_assert_true(g_str_has_suffix(m2->message_id().c_str(), "@mu.id")); + g_assert_true(g_str_has_suffix(m3->message_id().c_str(), "@mu.id")); +} + + +static void +test_message_fail () +{ + { + const auto msg = Message::make_from_path("/root/non-existent-path-12345"); + g_assert_false(!!msg); + } + + { + const auto msg = Message::make_from_text("", ""); + g_assert_false(!!msg); + } +} + +static void +test_message_sanitize_maildir() +{ + assert_equal(Message::sanitize_maildir("/"), "/"); + assert_equal(Message::sanitize_maildir("/foo/bar"), "/foo/bar"); + assert_equal(Message::sanitize_maildir("/foo/bar/cuux/"), "/foo/bar/cuux"); +} + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_add_func("/message/message/mailing-list", + test_message_mailing_list); + g_test_add_func("/message/message/attachments", + test_message_attachments); + g_test_add_func("/message/message/signed", + test_message_signed); + g_test_add_func("/message/message/signed-encrypted", + test_message_signed_encrypted); + g_test_add_func("/message/message/multipart-mixed-rfc822", + test_message_multipart_mixed_rfc822); + g_test_add_func("/message/message/detect-attachment", + test_message_detect_attachment); + g_test_add_func("/message/message/calendar", + test_message_calendar); + g_test_add_func("/message/message/references", + test_message_references); + g_test_add_func("/message/message/outlook-body", + test_message_outlook_body); + g_test_add_func("/message/message/message-id", + test_message_message_id); + g_test_add_func("/message/message/fail", + test_message_fail); + g_test_add_func("/message/message/sanitize-maildir", + test_message_sanitize_maildir); + + return g_test_run(); +} diff --git a/lib/mu-bookmarks.cc b/lib/mu-bookmarks.cc new file mode 100644 index 0000000..1865b64 --- /dev/null +++ b/lib/mu-bookmarks.cc @@ -0,0 +1,136 @@ +/* +** Copyright (C) 2010-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <glib.h> +#include "mu-bookmarks.hh" + +#define MU_BOOKMARK_GROUP "mu" + +struct MuBookmarks { + char* _bmpath; + GHashTable* _hash; +}; + +static void +fill_hash(GHashTable* hash, GKeyFile* kfile) +{ + gchar **keys, **cur; + + keys = g_key_file_get_keys(kfile, MU_BOOKMARK_GROUP, NULL, NULL); + if (!keys) + return; + + for (cur = keys; *cur; ++cur) { + gchar* val; + val = g_key_file_get_string(kfile, MU_BOOKMARK_GROUP, *cur, NULL); + if (val) + g_hash_table_insert(hash, *cur, val); + } + + /* don't use g_strfreev, because we put them in the hash table; + * only free the gchar** itself */ + g_free(keys); +} + +static GHashTable* +create_hash_from_key_file(const gchar* bmpath) +{ + GKeyFile* kfile; + GHashTable* hash; + + kfile = g_key_file_new(); + + if (!g_key_file_load_from_file(kfile, bmpath, G_KEY_FILE_NONE, NULL)) { + g_key_file_free(kfile); + return NULL; + } + + hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + fill_hash(hash, kfile); + + g_key_file_free(kfile); + + return hash; +} + +MuBookmarks* +mu_bookmarks_new(const gchar* bmpath) +{ + MuBookmarks* bookmarks; + GHashTable* hash; + + g_return_val_if_fail(bmpath, NULL); + + hash = create_hash_from_key_file(bmpath); + if (!hash) + return NULL; + + bookmarks = g_new(MuBookmarks, 1); + + bookmarks->_bmpath = g_strdup(bmpath); + bookmarks->_hash = hash; + + return bookmarks; +} + +void +mu_bookmarks_destroy(MuBookmarks* bm) +{ + if (!bm) + return; + + g_free(bm->_bmpath); + g_hash_table_destroy(bm->_hash); + g_free(bm); +} + +const gchar* +mu_bookmarks_lookup(MuBookmarks* bm, const gchar* name) +{ + g_return_val_if_fail(bm, NULL); + g_return_val_if_fail(name, NULL); + + return (const char*)g_hash_table_lookup(bm->_hash, name); +} + +struct _BMData { + MuBookmarksForeachFunc _func; + gpointer _user_data; +}; +typedef struct _BMData BMData; + +static void +each_bookmark(const gchar* key, const gchar* val, BMData* bmdata) +{ + bmdata->_func(key, val, bmdata->_user_data); +} + +void +mu_bookmarks_foreach(MuBookmarks* bm, MuBookmarksForeachFunc func, gpointer user_data) +{ + BMData bmdata; + + g_return_if_fail(bm); + g_return_if_fail(func); + + bmdata._func = func; + bmdata._user_data = user_data; + + g_hash_table_foreach(bm->_hash, (GHFunc)each_bookmark, &bmdata); +} diff --git a/lib/mu-bookmarks.hh b/lib/mu-bookmarks.hh new file mode 100644 index 0000000..a68d23a --- /dev/null +++ b/lib/mu-bookmarks.hh @@ -0,0 +1,76 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_BOOKMARKS_HH__ +#define MU_BOOKMARKS_HH__ + +#include <glib.h> +/** + * @addtogroup MuBookmarks + * Functions for dealing with bookmarks + * @{ + */ + +/*! \struct MuBookmarks + * \brief Opaque structure representing a sequence of bookmarks + */ +struct MuBookmarks; + +/** + * create a new bookmarks object. when it's no longer needed, use + * mu_bookmarks_destroy + * + * @param bmpath path to the bookmarks file + * + * @return a new BookMarks object, or NULL in case of error + */ +MuBookmarks* mu_bookmarks_new(const gchar* bmpath) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * destroy a bookmarks object + * + * @param bm a bookmarks object, or NULL + */ +void mu_bookmarks_destroy(MuBookmarks* bm); + +/** + * get the value for some bookmark + * + * @param bm a valid bookmarks object + * @param name name of the bookmark to retrieve + * + * @return the value of the bookmark or NULL in case in error, e.g. if + * the bookmark was not found + */ +const gchar* mu_bookmarks_lookup(MuBookmarks* bm, const gchar* name); + +typedef void (*MuBookmarksForeachFunc)(const gchar* key, const gchar* val, gpointer user_data); + +/** + * call a function for each bookmark + * + * @param bm a valid bookmarks object + * @param func a callback function to be called for each bookmarks + * @param user_data a user pointer passed to the callback + */ +void mu_bookmarks_foreach(MuBookmarks* bm, MuBookmarksForeachFunc func, gpointer user_data); + +/** @} */ + +#endif /*__MU_BOOKMARKS_H__*/ diff --git a/lib/mu-contacts-cache.cc b/lib/mu-contacts-cache.cc new file mode 100644 index 0000000..c4c8146 --- /dev/null +++ b/lib/mu-contacts-cache.cc @@ -0,0 +1,511 @@ +/* +** Copyright (C) 2019-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-contacts-cache.hh" + +#include <mutex> +#include <unordered_map> +#include <set> +#include <sstream> +#include <functional> +#include <algorithm> +#include <regex> +#include <ctime> + +#include <utils/mu-utils.hh> +#include <glib.h> + +using namespace Mu; + +struct EmailHash { + std::size_t operator()(const std::string& email) const { + return lowercase_hash(email); + } +}; +struct EmailEqual { + bool operator()(const std::string& email1, const std::string& email2) const { + return lowercase_hash(email1) == lowercase_hash(email2); + } +}; + +using ContactUMap = std::unordered_map<const std::string, Contact, EmailHash, EmailEqual>; +struct ContactsCache::Private { + Private(const std::string& serialized, const StringVec& personal) + : contacts_{deserialize(serialized)}, + personal_plain_{make_personal_plain(personal)}, + personal_rx_{make_personal_rx(personal)}, + dirty_{0} + {} + + ContactUMap deserialize(const std::string&) const; + std::string serialize() const; + + ContactUMap contacts_; + std::mutex mtx_; + + const StringVec personal_plain_; + const std::vector<std::regex> personal_rx_; + + size_t dirty_; + +private: + /** + * Return the non-regex addresses + * + * @param personal + * + * @return + */ + StringVec make_personal_plain(const StringVec& personal) const { + StringVec svec; + std::copy_if(personal.begin(), personal.end(), + std::back_inserter(svec), [&](auto&& p) { + return p.size() < 2 + || p.at(0) != '/' || p.at(p.length() - 1) != '/'; + }); + return svec; + } + + /** + * Return regexps for the regex-addresses + * + * @param personal + * + * @return + */ + std::vector<std::regex> make_personal_rx(const StringVec& personal) const { + std::vector<std::regex> rxvec; + for(auto&& p: personal) { + if (p.size() < 2 || p[0] != '/' || p[p.length()- 1] != '/') + continue; + // a regex pattern. + try { + const auto rxstr{p.substr(1, p.length() - 2)}; + rxvec.emplace_back(std::regex( + rxstr, std::regex::basic | std::regex::optimize | + std::regex::icase)); + } catch (const std::regex_error& rex) { + g_warning("invalid personal address regexp '%s': %s", + p.c_str(), + rex.what()); + } + } + return rxvec; + } +}; + +constexpr auto Separator = "\xff"; // Invalid in UTF-8 + + +ContactUMap +ContactsCache::Private::deserialize(const std::string& serialized) const +{ + ContactUMap contacts; + std::stringstream ss{serialized, std::ios_base::in}; + std::string line; + + while (getline(ss, line)) { + const auto parts = Mu::split(line, Separator); + if (G_UNLIKELY(parts.size() != 6)) { + g_warning("error: '%s'", line.c_str()); + continue; + } + Contact ci(parts[1], // email + std::move(parts[2]), // name + (time_t)g_ascii_strtoll(parts[4].c_str(), NULL, 10), // message_date + parts[3][0] == '1' ? true : false, // personal + (std::size_t)g_ascii_strtoll(parts[5].c_str(), NULL, 10), // frequency + g_get_monotonic_time()); // tstamp + contacts.emplace(std::move(parts[1]), std::move(ci)); + } + + return contacts; +} + +ContactsCache::ContactsCache(const std::string& serialized, const StringVec& personal) + : priv_{std::make_unique<Private>(serialized, personal)} +{ +} + +ContactsCache::~ContactsCache() = default; +std::string +ContactsCache::serialize() const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + std::string s; + + for (auto& item : priv_->contacts_) { + const auto& ci{item.second}; + s += Mu::format("%s%s" + "%s%s" + "%s%s" + "%d%s" + "%" G_GINT64_FORMAT "%s" + "%" G_GINT64_FORMAT "\n", + ci.display_name().c_str(), + Separator, + ci.email.c_str(), + Separator, + ci.name.c_str(), + Separator, + ci.personal ? 1 : 0, + Separator, + (gint64)ci.message_date, + Separator, + (gint64)ci.frequency); + } + + priv_->dirty_ = 0; + + return s; +} + +bool +ContactsCache::dirty() const +{ + return priv_->dirty_; +} + +//const Contact +void +ContactsCache::add(Contact&& contact) +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + ++priv_->dirty_; + + auto it = priv_->contacts_.find(contact.email); + + if (it == priv_->contacts_.end()) { // completely new contact + + contact.name = contact.name; + if (!contact.personal) + contact.personal = is_personal(contact.email); + contact.tstamp = g_get_monotonic_time(); + + auto email{contact.email}; + // return priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact))) + // .first->second; + + priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact))); + + } else { // existing contact. + auto& existing{it->second}; + ++existing.frequency; + if (contact.message_date > existing.message_date) { // update? + existing.email = std::move(contact.email); + // update name only if new one is not empty. + if (!contact.name.empty()) + existing.name = std::move(contact.name); + existing.tstamp = g_get_monotonic_time(); + existing.message_date = contact.message_date; + } + } +} + + +void +ContactsCache::add(Contacts&& contacts, bool& personal) +{ + personal = seq_find_if(contacts,[&](auto&& c){ + return is_personal(c.email); }) != contacts.cend(); + + for (auto&& contact: contacts) { + contact.personal = personal; + add(std::move(contact)); + } +} + + +const Contact* +ContactsCache::_find(const std::string& email) const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + const auto it = priv_->contacts_.find(email); + if (it == priv_->contacts_.end()) + return {}; + else + return &it->second; +} + +void +ContactsCache::clear() +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + ++priv_->dirty_; + + priv_->contacts_.clear(); +} + +std::size_t +ContactsCache::size() const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + return priv_->contacts_.size(); +} + + +/** + * This is used for sorting the Contacts in order of relevance. A highly + * specific algorithm, but the details don't matter _too_ much. + * + * This is currently used for the ordering in mu-cfind and auto-completion in + * mu4e, if the various completion methods don't override it... + */ +constexpr auto RecentOffset{15 * 24 * 3600}; +struct ContactLessThan { + ContactLessThan() + : recently_{::time({}) - RecentOffset} {} + + + bool operator()(const Mu::Contact& ci1, const Mu::Contact& ci2) const + { + // non-personal is less relevant. + if (ci1.personal != ci2.personal) + return ci1.personal < ci2.personal; + + // older is less relevant for recent messages + if (std::max(ci1.message_date, ci2.message_date) > recently_ && + ci1.message_date != ci2.message_date) + return ci1.message_date < ci2.message_date; + + // less frequent is less relevant + if (ci1.frequency != ci2.frequency) + return ci1.frequency < ci2.frequency; + + // if all else fails, alphabetically + return ci1.email < ci2.email; + } + // only sort recently seen contacts by recency; approx 15 days. + // this changes during the lifetime, but that's all fine. + const time_t recently_; +}; + +using ContactSet = std::set<std::reference_wrapper<const Contact>, + ContactLessThan>; + +void +ContactsCache::for_each(const EachContactFunc& each_contact) const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + // first sort them for 'rank' + ContactSet sorted; + for (const auto& item : priv_->contacts_) + sorted.emplace(item.second); + + // return in _reverse_ order, so we get the most relevant ones first. + for (auto it = sorted.rbegin(); it != sorted.rend(); ++it) { + if (!each_contact(*it)) + break; + } +} + +bool +ContactsCache::is_personal(const std::string& addr) const +{ + for (auto&& p : priv_->personal_plain_) + if (g_ascii_strcasecmp(addr.c_str(), p.c_str()) == 0) + return true; + + for (auto&& rx : priv_->personal_rx_) { + std::smatch m; // perhaps cache addr in personal_plain_? + if (std::regex_match(addr, m, rx)) + return true; + } + + return false; +} + +#ifdef BUILD_TESTS +/* + * Tests. + * + */ + +#include "utils/mu-test-utils.hh" + +static void +test_mu_contacts_cache_base() +{ + Mu::ContactsCache contacts(""); + + g_assert_true(contacts.empty()); + g_assert_cmpuint(contacts.size(), ==, 0); + + contacts.add(Mu::Contact("foo.bar@example.com", + "Foo", {}, 12345)); + g_assert_false(contacts.empty()); + g_assert_cmpuint(contacts.size(), ==, 1); + + contacts.add(Mu::Contact("cuux@example.com", "Cuux", {}, + 54321)); + + g_assert_cmpuint(contacts.size(), ==, 2); + + contacts.add( + Mu::Contact("foo.bar@example.com", "Foo", {}, 77777)); + g_assert_cmpuint(contacts.size(), ==, 2); + + contacts.add( + Mu::Contact("Foo.Bar@Example.Com", "Foo", {}, 88888)); + g_assert_cmpuint(contacts.size(), ==, 2); + // note: replaces first. + + { + const auto info = contacts._find("bla@example.com"); + g_assert_false(info); + } + + { + const auto info = contacts._find("foo.BAR@example.com"); + g_assert_true(info); + + g_assert_cmpstr(info->email.c_str(), ==, "Foo.Bar@Example.Com"); + } + + contacts.clear(); + g_assert_true(contacts.empty()); + g_assert_cmpuint(contacts.size(), ==, 0); +} + +static void +test_mu_contacts_cache_personal() +{ + Mu::StringVec personal = {"foo@example.com", "bar@cuux.org", "/bar-.*@fnorb.f./"}; + Mu::ContactsCache contacts{"", personal}; + + g_assert_true(contacts.is_personal("foo@example.com")); + g_assert_true(contacts.is_personal("Bar@CuuX.orG")); + g_assert_true(contacts.is_personal("bar-123abc@fnorb.fi")); + g_assert_true(contacts.is_personal("bar-zzz@fnorb.fr")); + + g_assert_false(contacts.is_personal("foo@bar.com")); + g_assert_false(contacts.is_personal("BÂr@CuuX.orG")); + g_assert_false(contacts.is_personal("bar@fnorb.fi")); + g_assert_false(contacts.is_personal("bar-zzz@fnorb.xr")); +} + + +static void +test_mu_contacts_cache_foreach() +{ + Mu::ContactsCache ccache(""); + ccache.add(Mu::Contact{"a@example.com", "a", 123, true, 1000, 0}); + ccache.add(Mu::Contact{"b@example.com", "b", 456, true, 1000, 0}); + + { + size_t n{}; + g_assert_false(ccache.empty()); + g_assert_cmpuint(ccache.size(),==,2); + ccache.for_each([&](auto&& contact) { ++n; return false; }); + g_assert_cmpuint(n,==,1); + } + + { + size_t n{}; + g_assert_false(ccache.empty()); + g_assert_cmpuint(ccache.size(),==,2); + ccache.for_each([&](auto&& contact) { ++n; return true; }); + g_assert_cmpuint(n,==,2); + } + + { + size_t n{}; + ccache.clear(); + g_assert_true(ccache.empty()); + g_assert_cmpuint(ccache.size(),==,0); + ccache.for_each([&](auto&& contact) { ++n; return true; }); + g_assert_cmpuint(n,==,0); + } +} + + + +static void +test_mu_contacts_cache_sort() +{ + auto result_chars = [](const Mu::ContactsCache& ccache)->std::string { + std::string str; + if (g_test_verbose()) + g_print("contacts-cache:\n"); + + ccache.for_each([&](auto&& contact) { + if (g_test_verbose()) + g_print("\t- %s\n", contact.display_name().c_str()); + str += contact.name; + return true; + }); + return str; + }; + + const auto now{std::time({})}; + + // "first" means more relevant + + { /* recent messages, newer comes first */ + + Mu::ContactsCache ccache(""); + ccache.add(Mu::Contact{"a@example.com", "a", now, true, 1000, 0}); + ccache.add(Mu::Contact{"b@example.com", "b", now-1, true, 1000, 0}); + assert_equal(result_chars(ccache), "ab"); + } + + { /* non-recent messages, more frequent comes first */ + + Mu::ContactsCache ccache(""); + ccache.add(Mu::Contact{"a@example.com", "a", now-2*RecentOffset, true, 1000, 0}); + ccache.add(Mu::Contact{"b@example.com", "b", now-3*RecentOffset, true, 2000, 0}); + assert_equal(result_chars(ccache), "ba"); + } + + { /* personal comes first */ + + Mu::ContactsCache ccache(""); + ccache.add(Mu::Contact{"a@example.com", "a", now-5*RecentOffset, true, 1000, 0}); + ccache.add(Mu::Contact{"b@example.com", "b", now, false, 8000, 0}); + assert_equal(result_chars(ccache), "ab"); + } + + { /* if all else fails, reverse-alphabetically */ + Mu::ContactsCache ccache(""); + ccache.add(Mu::Contact{"a@example.com", "a", now, false, 1000, 0}); + ccache.add(Mu::Contact{"b@example.com", "b", now, false, 1000, 0}); + g_assert_cmpuint(ccache.size(),==,2); + assert_equal(result_chars(ccache), "ba"); + } +} + + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_add_func("/lib/contacts-cache/base", test_mu_contacts_cache_base); + g_test_add_func("/lib/contacts-cache/personal", test_mu_contacts_cache_personal); + g_test_add_func("/lib/contacts-cache/for-each", test_mu_contacts_cache_foreach); + g_test_add_func("/lib/contacts-cache/sort", test_mu_contacts_cache_sort); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/mu-contacts-cache.hh b/lib/mu-contacts-cache.hh new file mode 100644 index 0000000..7c871d7 --- /dev/null +++ b/lib/mu-contacts-cache.hh @@ -0,0 +1,159 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_CONTACTS_CACHE_HH__ +#define __MU_CONTACTS_CACHE_HH__ + +#include <glib.h> +#include <time.h> +#include <memory> +#include <functional> +#include <chrono> +#include <string> +#include <time.h> +#include <inttypes.h> +#include <utils/mu-utils.hh> + +#include <message/mu-message.hh> + +namespace Mu { + +class ContactsCache { +public: + /** + * Construct a new ContactsCache object + * + * @param serialized serialized contacts + * @param personal personal addresses + */ + ContactsCache(const std::string& serialized = "", const StringVec& personal = {}); + + /** + * DTOR + * + */ + ~ContactsCache(); + + /** + * Add a contact + * + * @param contact a Contact object + * + */ + void add(Contact&& contact); + + + /** + * Add a contacts sequence; this should be used for the contacts of a + * specific message, and determines if it is a "personal" message: + * if any of the contacts matches one of the personal addresses, + * any of the senders/recipients are considered "personal" + * + * @param contacts a Contact object sequence + * @param is_personal receives true if any of the contacts was personal; + * false otherwise + */ + void add(Contacts&& contacts, bool& is_personal); + void add(Contacts&& contacts) { + bool _ignore; + add(std::move(contacts), _ignore); + } + + + /** + * Clear all contacts + * + */ + void clear(); + + /** + * Get the number of contacts + * + + * @return number of contacts + */ + std::size_t size() const; + + /** + * Are there no contacts? + * + * @return true or false + */ + bool empty() const { return size() == 0; } + + /** + * Get the contacts, serialized. This all marks the data as + * non-dirty (see dirty()) + * + * @return serialized contacts + */ + std::string serialize() const; + + /** + * Has the contacts database change since the last + * call to serialize()? + * + * @return true or false + */ + bool dirty() const; + + /** + * Does this look like a 'personal' address? + * + * @param addr some e-mail address + * + * @return true or false + */ + bool is_personal(const std::string& addr) const; + + /** + * Find a contact based on the email address. This is not safe, since + * the returned ptr can be invalidated at any time; only for unit-tests. + * + * @param email email address + * + * @return contact info, or {} if not found + */ + const Contact* _find(const std::string& email) const; + + /** + * Prototype for a callable that receives a contact + * + * @param contact some contact + * + * @return to get more contacts; false otherwise + */ + using EachContactFunc = std::function<bool(const Contact& contact_info)>; + + /** + * Invoke some callable for each contact, in _descending_ order of rank (i.e., the + * highest ranked contacts come first). + * + * @param each_contact function invoked for each contact + */ + void for_each(const EachContactFunc& each_contact) const; + +private: + struct Private; + std::unique_ptr<Private> priv_; +}; + +} // namespace Mu + +#endif /* __MU_CONTACTS_CACHE_HH__ */ diff --git a/lib/mu-maildir.cc b/lib/mu-maildir.cc new file mode 100644 index 0000000..8b27aa7 --- /dev/null +++ b/lib/mu-maildir.cc @@ -0,0 +1,458 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to 59the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include <string> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <stdlib.h> + +#include <string.h> +#include <errno.h> +#include <glib/gprintf.h> +#include <gio/gio.h> + +#include "glibconfig.h" +#include "mu-maildir.hh" +#include "utils/mu-utils.hh" +#include "utils/mu-util.h" + +using namespace Mu; + +#define MU_MAILDIR_NOINDEX_FILE ".noindex" +#define MU_MAILDIR_NOUPDATE_FILE ".noupdate" + +/* On Linux (and some BSD), we have entry->d_type, but some file + * systems (XFS, ReiserFS) do not support it, and set it DT_UNKNOWN. + * On other OSs, notably Solaris, entry->d_type is not present at all. + * For these cases, we use lstat (in get_dtype) as a slower fallback, + * and return it in the d_type parameter + */ +static unsigned char +get_dtype(struct dirent* dentry, const std::string& path, bool use_lstat) +{ +#ifdef HAVE_STRUCT_DIRENT_D_TYPE + + if (dentry->d_type == DT_UNKNOWN) + goto slowpath; + if (dentry->d_type == DT_LNK && !use_lstat) + goto slowpath; + + return dentry->d_type; /* fastpath */ + +slowpath: +#endif /*HAVE_STRUCT_DIRENT_D_TYPE*/ + return mu_util_get_dtype(path.c_str(), use_lstat); +} + +static Mu::Result<void> +create_maildir(const std::string& path, mode_t mode) +{ + if (path.empty()) + return Err(Error{Error::Code::File, "path must not be empty"}); + + std::array<std::string,3> subdirs = {"new", "cur", "tmp"}; + for (auto&& subdir: subdirs) { + + const auto fullpath{path + G_DIR_SEPARATOR_S + subdir}; + + /* if subdir already exists, don't try to re-create + * it */ + if (mu_util_check_dir(fullpath.c_str(), TRUE, TRUE)) + continue; + + int rv{g_mkdir_with_parents(fullpath.c_str(), static_cast<int>(mode))}; + + /* note, g_mkdir_with_parents won't detect an error if + * there's already such a dir, but with the wrong + * permissions; so we need to check */ + if (rv != 0 || !mu_util_check_dir(fullpath.c_str(), TRUE, TRUE)) + return Err(Error{Error::Code::File, + "creating dir failed for %s: %s", + fullpath.c_str(), g_strerror(errno)}); + } + + return Ok(); +} + +static Mu::Result<void> /* create a noindex file if requested */ +create_noindex(const std::string& path) +{ + const auto noindexpath{path + G_DIR_SEPARATOR_S MU_MAILDIR_NOINDEX_FILE}; + + /* note, if the 'close' failed, creation may still have succeeded...*/ + int fd = ::creat(noindexpath.c_str(), 0644); + if (fd < 0 || ::close(fd) != 0) + return Err(Error{Error::Code::File, + "error creating .noindex: %s", g_strerror(errno)}); + else + return Ok(); +} + +Mu::Result<void> +Mu::maildir_mkdir(const std::string& path, mode_t mode, bool noindex) +{ + if (auto&& created{create_maildir(path, mode)}; !created) + return created; // fail. + else if (!noindex) + return Ok(); + + if (auto&& created{create_noindex(path)}; !created) + return created; //fail + + return Ok(); +} + +/* determine whether the source message is in 'new' or in 'cur'; + * we ignore messages in 'tmp' for obvious reasons */ +static Mu::Result<void> +check_subdir(const std::string& src, bool& in_cur) +{ + char *srcpath{g_path_get_dirname(src.c_str())}; + + bool invalid{}; + if (g_str_has_suffix(srcpath, "cur")) + in_cur = true; + else if (g_str_has_suffix(srcpath, "new")) + in_cur = false; + else + invalid = true; + + g_free(srcpath); + + if (invalid) + return Err(Error{Error::Code::File, "invalid source message '%s'", + src.c_str()}); + else + return Ok(); +} + +static Mu::Result<std::string> +get_target_fullpath(const std::string& src, const std::string& targetpath, + bool unique_names) +{ + bool in_cur{}; + if (auto&& res = check_subdir(src, in_cur); !res) + return Err(std::move(res.error())); + + char *srcfile{g_path_get_basename(src.c_str())}; + + /* create targetpath; note: make the filename *cough* unique by + * including a hash of the srcname in the targetname. This helps if + * there are copies of a message (which all have the same basename) + */ + std::string fulltargetpath; + if (unique_names) + fulltargetpath = format("%s%c%s%c%u_%s", + targetpath.c_str(), + G_DIR_SEPARATOR, in_cur ? "cur" : "new", + G_DIR_SEPARATOR, + g_str_hash(src.c_str()), + srcfile); + else + fulltargetpath = format("%s%c%s%c%s", + targetpath.c_str(), + G_DIR_SEPARATOR, in_cur ? "cur" : "new", + G_DIR_SEPARATOR, + srcfile); + g_free(srcfile); + + return fulltargetpath; +} + +Result<void> +Mu::maildir_link(const std::string& src, const std::string& targetpath, + bool unique_names) +{ + auto path_res{get_target_fullpath(src, targetpath, unique_names)}; + if (!path_res) + return Err(std::move(path_res.error())); + + auto rv{::symlink(src.c_str(), path_res->c_str())}; + if (rv != 0) + return Err(Error{Error::Code::File, + "error creating link %s => %s: %s", + path_res->c_str(), + src.c_str(), + g_strerror(errno)}); + + return Ok(); +} + +static bool +clear_links(const std::string& path, DIR* dir) +{ + bool res; + struct dirent* dentry; + + res = true; + errno = 0; + + while ((dentry = ::readdir(dir))) { + + if (dentry->d_name[0] == '.') + continue; /* ignore .,.. other dotdirs */ + + const auto fullpath{ + format("%s" G_DIR_SEPARATOR_S "%s",path.c_str(), dentry->d_name)}; + const auto d_type = get_dtype(dentry, fullpath.c_str(), true/*lstat*/); + switch(d_type) { + case DT_LNK: + if (::unlink(fullpath.c_str()) != 0) { + g_warning("error unlinking %s: %s", + fullpath.c_str(), g_strerror(errno)); + res = false; + } + break; + case DT_DIR: { + DIR* subdir{::opendir(fullpath.c_str())}; + if (!subdir) { + g_warning("failed to open dir %s: %s", fullpath.c_str(), + g_strerror(errno)); + res = false; + } + if (!clear_links(fullpath, subdir)) + res = false; + ::closedir(subdir); + } + break; + default: + break; + } + } + + return res; +} + +Mu::Result<void> +Mu::maildir_clear_links(const std::string& path) +{ + const auto dir{::opendir(path.c_str())}; + if (!dir) + return Err(Error{Error::Code::File, "failed to open %s: %s", + path.c_str(), g_strerror(errno)}); + + clear_links(path, dir); + ::closedir(dir); + + return Ok(); +} + +static Mu::Result<void> +msg_move_verify(const std::string& src, const std::string& dst) +{ + /* double check -- is the target really there? */ + if (::access(dst.c_str(), F_OK) != 0) + return Err(Error{Error::Code::File, + "can't find target (%s->%s)", + src.c_str(), dst.c_str()}); + + if (::access(src.c_str(), F_OK) == 0) { + if (src == dst) { + g_warning("moved %s to itself", src.c_str()); + } + /* this could happen if some other tool (for mail syncing) is + * interfering */ + g_debug("the source is still there (%s->%s)", src.c_str(), dst.c_str()); + } + + return Ok(); +} + +/* use GIO to move files; this is slower than rename() so only use + * this when needed: when moving across filesystems */ +static Mu::Result<void> +msg_move_g_file(const std::string& src, const std::string& dst) +{ + GFile *srcfile{g_file_new_for_path(src.c_str())}; + GFile *dstfile{g_file_new_for_path(dst.c_str())}; + + GError* err{}; + auto res = g_file_move(srcfile, dstfile, + G_FILE_COPY_OVERWRITE, + NULL, NULL, NULL, &err); + g_clear_object(&srcfile); + g_clear_object(&dstfile); + + if (res) + return Ok(); + else + return Err(Error{Error::Code::File, &err/*consumed*/, + "error moving %s -> %s", + src.c_str(), dst.c_str()}); +} + +static Mu::Result<void> +msg_move(const std::string& src, const std::string& dst, bool force_gio) +{ + if (::access(src.c_str(), R_OK) != 0) + return Err(Error{Error::Code::File, "cannot read %s", src.c_str()}); + + if (!force_gio) { /* for testing */ + + if (::rename(src.c_str(), dst.c_str()) == 0) /* seems it worked; double-check */ + return msg_move_verify(src, dst); + + if (errno != EXDEV) /* some unrecoverable error occurred */ + return Err(Error{Error::Code::File, "error moving %s -> %s: %s", + src.c_str(), dst.c_str(), strerror(errno)}); + } + + /* the EXDEV / force-gio case -- source and target live on different + * filesystems */ + auto res = msg_move_g_file(src, dst); + if (!res) + return res; + else + return msg_move_verify(src, dst); +} + + +Mu::Result<void> +Mu::maildir_move_message(const std::string& oldpath, + const std::string& newpath, + bool force_gio) +{ + if (oldpath == newpath) + return Ok(); // nothing to do. + + g_debug("moving %s --> %s", oldpath.c_str(), newpath.c_str()); + return msg_move(oldpath, newpath, force_gio); +} + +static std::string +reinvent_filename_base() +{ + return format("%u.%08x%08x.%s", + static_cast<unsigned>(::time(NULL)), + g_random_int(), + static_cast<uint32_t>(g_get_monotonic_time()), + g_get_host_name()); +} + +/** + * Determine the destination filename + * + * @param file a filename + * @param flags flags for the destination + * @param new_name whether to change the basename + * + * @return the destion filename. + */ +static std::string +determine_dst_filename(const std::string& file, Flags flags, + bool new_name) +{ + /* Recalculate a unique new base file name */ + auto&& parts{message_file_parts(file)}; + if (new_name) + parts.base = reinvent_filename_base(); + + /* for a New message, there are no flags etc.; so we only return the + * name sans suffix */ + if (any_of(flags & Flags::New)) + return std::move(parts.base); + + const auto flagstr{ + to_string( + flags_filter( + flags, MessageFlagCategory::Mailfile))}; + + return parts.base + parts.separator + "2," + flagstr; +} + + +/* + * sanity checks + */ +static Mu::Result<void> +check_determine_target_params (const std::string& old_path, + const std::string& root_maildir_path, + const std::string& target_maildir, + Flags newflags) +{ + if (!g_path_is_absolute(old_path.c_str())) + return Err(Error{Error::Code::File, + "old_path is not absolute (%s)", old_path.c_str()}); + + if (!g_path_is_absolute(root_maildir_path.c_str())) + return Err(Error{Error::Code::File, + "root maildir path is not absolute", + root_maildir_path.c_str()}); + + if (!target_maildir.empty() && target_maildir[0] != '/') + return Err(Error{Error::Code::File, + "target maildir must be empty or start with / (%s)", + target_maildir.c_str()}); + + if (old_path.find(root_maildir_path) != 0) + return Err(Error{Error::Code::File, + "old-path must be below root-maildir (%s) (%s)", + old_path.c_str(), root_maildir_path.c_str()}); + + if (any_of(newflags & Flags::New) && newflags != Flags::New) + return Err(Error{Error::Code::File, + "if ::New is specified, " + "it must be the only flag"}); + return Ok(); +} + + +Mu::Result<std::string> +Mu::maildir_determine_target(const std::string& old_path, + const std::string& root_maildir_path, + const std::string& target_maildir, + Flags newflags, + bool new_name) +{ + /* sanity checks */ + if (const auto checked{check_determine_target_params( + old_path, root_maildir_path, target_maildir, newflags)}; !checked) + return Err(Error{std::move(checked.error())}); + + /* + * this gets us the source maildir filesystem path, the directory + * in which new/ & cur/ lives, and the source file + */ + const auto src{base_message_dir_file(old_path)}; + if (!src) + return Err(src.error()); + const auto& [src_mdir, src_file, is_new] = *src; + + /* if target_mdir is empty, the src_dir does not change (though cur/ + * maybe become new or vice-versa) */ + const auto dst_mdir{target_maildir.empty() ? src_mdir : + root_maildir_path + target_maildir}; + + /* now calculate the message name (incl. its immediate parent dir) */ + const auto dst_file{determine_dst_filename(src_file, newflags, new_name)}; + + /* and the complete path name. */ + const auto subdir = std::invoke([&]()->std::string { + if (none_of(newflags & Flags::New)) + return "cur"; + else + return "new"; + }); + + return dst_mdir + G_DIR_SEPARATOR_S + subdir + G_DIR_SEPARATOR_S + dst_file; +} diff --git a/lib/mu-maildir.hh b/lib/mu-maildir.hh new file mode 100644 index 0000000..42a49a3 --- /dev/null +++ b/lib/mu-maildir.hh @@ -0,0 +1,123 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_MAILDIR_HH__ +#define MU_MAILDIR_HH__ + +#include <string> +#include <utils/mu-result.hh> + +#include <glib.h> +#include <time.h> +#include <sys/types.h> /* for mode_t */ +#include <message/mu-message.hh> + +namespace Mu { + +/** + * Create a new maildir. if parts of the maildir already exists, those will + * simply be ignored. + * + * IOW, if you try to create the same maildir twice, the second will simply be a + * no-op (without any errors). Note, if the function fails 'halfway', it will + * *not* try to remove the parts the were created. it *will* create any parent + * dirs that are not yet existent. + * + * @param path the path (missing components will be created, as in 'mkdir -p'). + * must be non-empty + * @param mode the file mode (e.g., 0755) + * @param noindex add a .noindex file to the maildir, so it will be excluded + * from indexing by 'mu index' + * + * @return a valid result (!!result) or an Error + */ +Result<void> maildir_mkdir(const std::string& path, mode_t mode=0700, + bool noindex=false); + +/** + * Create a symbolic link to a mail message + * + * @param src the full path to the source message + * @param targetpath the path to the target maildir; ie., *not* + * MyMaildir/cur, but just MyMaildir/. The function will figure out + * the correct subdir then. + * @param unique_names whether to create unique names; should be true unless + * for tests. + * + * @return a valid result (!!result) or an Error + */ +Result<void> maildir_link(const std::string& src, const std::string& targetpath, + bool unique_names=true); + +/** + * Recursively delete all the symbolic links in a directory tree + * + * @param dir top dir + * + * @return a valid result (!!result) or an Error + */ +Result<void> maildir_clear_links(const std::string& dir); + +/** + * Move a message file to another maildir. If the target exists, it is + * overwritten. + * + * @param oldpath an absolute file system path to an existing message in an + * actual maildir + * @param newpath the absolete full path to the target file + * @param force_gio force the use of GIO for moving; this is done automatically + * when needed; forcing is mostly useful for tests + * + * @return a valid result (!!result) or an Error + */ +Result<void> maildir_move_message(const std::string& oldpath, + const std::string& newpath, + bool force_gio = false); + +/** + * Determine the target path for a to-be-moved message; i.e. this does not + * actually move the message, only calculate the path. + * + * @param old_path an absolute file system path to an existing message in an + * actual maildir + * @param root_maildir_path the absolete file system path under which + * all maidlirs live. + * @param target_maildir the target maildir; note that this the base-level + * Maildir, ie. /home/user/Maildir/archive, and must _not_ include the + * 'cur' or 'new' part. Note that the target maildir must be on the + * same filesystem. Can be empty if the message should not be moved to + * a different maildir; note that this may still involve a + * move to another directory (say, from new/ to cur/) + * @param flags to set for the target (influences the filename, path). + * @param new_name whether to change the basename of the file + * @param err receives error information + * + * @return Full path name of the target file or std::nullopt in case + * of error + */ +Result<std::string> +maildir_determine_target(const std::string& old_path, + const std::string& root_maildir_path, + const std::string& target_maildir, + Flags newflags, + bool new_name); + +} // namespace Mu + +#endif /*MU_MAILDIR_HH__*/ diff --git a/lib/mu-parser.cc b/lib/mu-parser.cc new file mode 100644 index 0000000..3cde7c5 --- /dev/null +++ b/lib/mu-parser.cc @@ -0,0 +1,506 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ +#include "mu-parser.hh" + +#include <algorithm> +#include <regex> +#include <limits> + +#include "mu-tokenizer.hh" +#include "utils/mu-utils.hh" +#include "utils/mu-error.hh" +#include "message/mu-message.hh" + +using namespace Mu; + +// 3 precedence levels: units (NOT,()) > factors (OR) > terms (AND) + +// query -> <term-1> | ε +// <term-1> -> <factor-1> <term-2> | ε +// <term-2> -> OR|XOR <term-1> | ε +// <factor-1> -> <unit> <factor-2> | ε +// <factor-2> -> [AND]|AND NOT <factor-1> | ε +// <unit> -> [NOT] <term-1> | ( <term-1> ) | <data> +// <data> -> <value> | <range> | <regex> +// <value> -> [field:]value +// <range> -> [field:][lower]..[upper] +// <regex> -> [field:]/regex/ + +#define BUG(...) \ + Mu::Error(Error::Code::Internal, format("%u: BUG: ", __LINE__) + format(__VA_ARGS__)) + +/** + * Get the "shortcut"/internal fields for the the given fieldstr or empty if there is none + * + * @param fieldstr a fieldstr, e.g "subject" or "s" for the subject field + * + * @return a vector with "exploded" values, with a code and a fullname. E.g. "s" might map + * to [<"S","subject">], while "recip" could map to [<"to", "T">, <"cc", "C">, <"bcc", "B">] + */ +struct FieldInfo { + const std::string field; + const std::string prefix; + bool supports_phrase; + Field::Id id; +}; +using FieldInfoVec = std::vector<FieldInfo>; +struct Parser::Private { + Private(const Store& store, Parser::Flags flags) : store_{store}, flags_{flags} {} + + std::vector<std::string> process_regex(const std::string& field, + const std::regex& rx) const; + + Mu::Tree term_1(Mu::Tokens& tokens, WarningVec& warnings) const; + Mu::Tree term_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const; + Mu::Tree factor_1(Mu::Tokens& tokens, WarningVec& warnings) const; + Mu::Tree factor_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const; + Mu::Tree unit(Mu::Tokens& tokens, WarningVec& warnings) const; + Mu::Tree data(Mu::Tokens& tokens, WarningVec& warnings) const; + Mu::Tree range(const FieldInfoVec& fields, + const std::string& lower, + const std::string& upper, + size_t pos, + WarningVec& warnings) const; + Mu::Tree regex(const FieldInfoVec& fields, + const std::string& v, + size_t pos, + WarningVec& warnings) const; + Mu::Tree value(const FieldInfoVec& fields, + const std::string& v, + size_t pos, + WarningVec& warnings) const; + + private: + const Store& store_; + const Parser::Flags flags_; +}; + +static std::string +process_value(const std::string& field, const std::string& value) +{ + const auto id_opt{field_from_name(field)}; + if (id_opt) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (id_opt->id) { + case Field::Id::Priority: { + if (!value.empty()) + return std::string(1, value[0]); + } break; + case Field::Id::Flags: + if (const auto info{flag_info(value)}; info) + return std::string(1, info->shortcut_lower()); + break; + default: + break; + } +#pragma GCC diagnostic pop + } + + return value; // XXX prio/flags, etc. alias +} + +static void +add_field(std::vector<FieldInfo>& fields, Field::Id field_id) +{ + const auto field{field_from_id(field_id)}; + if (!field.shortcut) + return; // can't be searched + + fields.emplace_back(FieldInfo{std::string{field.name}, field.xapian_term(), + field.is_indexable_term(), field_id}); +} + +static std::vector<FieldInfo> +process_field(const std::string& field_str, Parser::Flags flags) +{ + std::vector<FieldInfo> fields; + if (any_of(flags & Parser::Flags::UnitTest)) { + add_field(fields, Field::Id::MessageId); + return fields; + } + + if (field_str == "contact" || field_str == "recip") { // multi fields + add_field(fields, Field::Id::To); + add_field(fields, Field::Id::Cc); + add_field(fields, Field::Id::Bcc); + if (field_str == "contact") + add_field(fields, Field::Id::From); + } else if (field_str.empty()) { + add_field(fields, Field::Id::To); + add_field(fields, Field::Id::Cc); + add_field(fields, Field::Id::Bcc); + add_field(fields, Field::Id::From); + add_field(fields, Field::Id::Subject); + add_field(fields, Field::Id::BodyText); + } else if (const auto field_opt{field_from_name(field_str)}; field_opt) + add_field(fields, field_opt->id); + + return fields; +} + +static bool +is_range_field(const std::string& field_str) +{ + if (const auto field_opt{field_from_name(field_str)}; !field_opt) + return false; + else + return field_opt->is_range(); +} + +struct MyRange { + std::string lower; + std::string upper; +}; + +static MyRange +process_range(const std::string& field_str, + const std::string& lower, const std::string& upper) +{ + const auto field_opt{field_from_name(field_str)}; + if (!field_opt) + return {lower, upper}; + + std::string l2 = lower; + std::string u2 = upper; + constexpr auto upper_limit = std::numeric_limits<int64_t>::max(); + + if (field_opt->id == Field::Id::Date || field_opt->id == Field::Id::Changed) { + l2 = to_lexnum(parse_date_time(lower, true).value_or(0)); + u2 = to_lexnum(parse_date_time(upper, false).value_or(upper_limit)); + } else if (field_opt->id == Field::Id::Size) { + l2 = to_lexnum(parse_size(lower, true).value_or(0)); + u2 = to_lexnum(parse_size(upper, false).value_or(upper_limit)); + } + + return {l2, u2}; +} + +std::vector<std::string> +Parser::Private::process_regex(const std::string& field_str, + const std::regex& rx) const +{ + const auto field_opt{field_from_name(field_str)}; + if (!field_opt) + return {}; + + const auto prefix{field_opt->xapian_term()}; + std::vector<std::string> terms; + store_.for_each_term(field_opt->id, [&](auto&& str) { + auto val{str.c_str() + 1}; // strip off the Xapian prefix. + if (std::regex_search(val, rx)) + terms.emplace_back(std::move(val)); + return true; + }); + + return terms; +} + +static Token +look_ahead(const Mu::Tokens& tokens) +{ + return tokens.front(); +} + +static Mu::Tree +empty() +{ + return {{Node::Type::Empty}}; +} + +Mu::Tree +Parser::Private::value(const FieldInfoVec& fields, + const std::string& v, + size_t pos, + WarningVec& warnings) const +{ + auto val = utf8_flatten(v); + + if (fields.empty()) + throw BUG("expected one or more fields"); + + if (fields.size() == 1) { + const auto item = fields.front(); + return Tree({Node::Type::Value, + FieldValue{item.id, process_value(item.field, val)}}); + } + + // a 'multi-field' such as "recip:" + Tree tree(Node{Node::Type::OpOr}); + for (const auto& item : fields) + tree.add_child(Tree({Node::Type::Value, + FieldValue{item.id, + process_value(item.field, val)}})); + return tree; +} + +Mu::Tree +Parser::Private::regex(const FieldInfoVec& fields, + const std::string& v, + size_t pos, + WarningVec& warnings) const +{ + if (v.length() < 2) + throw BUG("expected regexp, got '%s'", v.c_str()); + + const auto rxstr = utf8_flatten(v.substr(1, v.length() - 2)); + + try { + Tree tree(Node{Node::Type::OpOr}); + const auto rx = std::regex(rxstr); + for (const auto& field : fields) { + const auto terms = process_regex(field.field, rx); + for (const auto& term : terms) { + tree.add_child(Tree({Node::Type::Value, + FieldValue{field.id, term}})); + } + } + + if (tree.children.empty()) + return empty(); + else + return tree; + + } catch (...) { + // fallback + warnings.push_back({pos, "invalid regexp"}); + return value(fields, v, pos, warnings); + } +} + +Mu::Tree +Parser::Private::range(const FieldInfoVec& fields, + const std::string& lower, + const std::string& upper, + size_t pos, + WarningVec& warnings) const +{ + if (fields.empty()) + throw BUG("expected field"); + + const auto& field = fields.front(); + if (!is_range_field(field.field)) + return value(fields, lower + ".." + upper, pos, warnings); + + auto prange = process_range(field.field, lower, upper); + if (prange.lower > prange.upper) + prange = process_range(field.field, upper, lower); + + return Tree({Node::Type::Range, + FieldValue{field.id, prange.lower, prange.upper}}); +} + +Mu::Tree +Parser::Private::data(Mu::Tokens& tokens, WarningVec& warnings) const +{ + const auto token = look_ahead(tokens); + if (token.type != Token::Type::Data) + warnings.push_back({token.pos, "expected: value"}); + + tokens.pop_front(); + + std::string field, val; + const auto col = token.str.find(":"); + if (col != 0 && col != std::string::npos && col != token.str.length() - 1) { + field = token.str.substr(0, col); + val = token.str.substr(col + 1); + } else + val = token.str; + + auto fields = process_field(field, flags_); + if (fields.empty()) { // not valid field... + warnings.push_back({token.pos, format("invalid field '%s'", field.c_str())}); + fields = process_field("", flags_); + // fallback, treat the whole of foo:bar as a value + return value(fields, field + ":" + val, token.pos, warnings); + } + + // does it look like a regexp? + if (val.length() >= 2) + if (val[0] == '/' && val[val.length() - 1] == '/') + return regex(fields, val, token.pos, warnings); + + // does it look like a range? + const auto dotdot = val.find(".."); + if (dotdot != std::string::npos) + return range(fields, + val.substr(0, dotdot), + val.substr(dotdot + 2), + token.pos, + warnings); + else if (is_range_field(fields.front().field)) { + // range field without a range - treat as field:val..val + return range(fields, val, val, token.pos, warnings); + } + + // if nothing else, it's a value. + return value(fields, val, token.pos, warnings); +} + +Mu::Tree +Parser::Private::unit(Mu::Tokens& tokens, WarningVec& warnings) const +{ + if (tokens.empty()) { + warnings.push_back({0, "expected: unit"}); + return empty(); + } + + const auto token = look_ahead(tokens); + + if (token.type == Token::Type::Not) { + tokens.pop_front(); + Tree tree{{Node::Type::OpNot}}; + tree.add_child(unit(tokens, warnings)); + return tree; + } + + if (token.type == Token::Type::Open) { + tokens.pop_front(); + auto tree = term_1(tokens, warnings); + if (tokens.empty()) + warnings.push_back({token.pos, "expected: ')'"}); + else { + const auto token2 = look_ahead(tokens); + if (token2.type == Token::Type::Close) + tokens.pop_front(); + else { + warnings.push_back( + {token2.pos, + std::string("expected: ')' but got ") + token2.str}); + } + } + return tree; + } + + return data(tokens, warnings); +} + +Mu::Tree +Parser::Private::factor_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const +{ + if (tokens.empty()) + return empty(); + + const auto token = look_ahead(tokens); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (token.type) { + case Token::Type::And: { + tokens.pop_front(); + op = Node::Type::OpAnd; + } break; + + case Token::Type::Open: + case Token::Type::Data: + case Token::Type::Not: + op = Node::Type::OpAnd; // implicit AND + break; + + default: + return empty(); + } +#pragma GCC diagnostic pop + + return factor_1(tokens, warnings); +} + +Mu::Tree +Parser::Private::factor_1(Mu::Tokens& tokens, WarningVec& warnings) const +{ + Node::Type op{Node::Type::Invalid}; + + auto t = unit(tokens, warnings); + auto a2 = factor_2(tokens, op, warnings); + + if (a2.empty()) + return t; + + Tree tree{{op}}; + tree.add_child(std::move(t)); + tree.add_child(std::move(a2)); + + return tree; +} + +Mu::Tree +Parser::Private::term_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const +{ + if (tokens.empty()) + return empty(); + + const auto token = look_ahead(tokens); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (token.type) { + case Token::Type::Or: op = Node::Type::OpOr; break; + case Token::Type::Xor: op = Node::Type::OpXor; break; + default: + if (token.type != Token::Type::Close) + warnings.push_back({token.pos, "expected OR|XOR"}); + return empty(); + } +#pragma GCC diagnostic pop + + tokens.pop_front(); + + return term_1(tokens, warnings); +} + +Mu::Tree +Parser::Private::term_1(Mu::Tokens& tokens, WarningVec& warnings) const +{ + Node::Type op{Node::Type::Invalid}; + + auto t = factor_1(tokens, warnings); + auto o2 = term_2(tokens, op, warnings); + + if (o2.empty()) + return t; + else { + Tree tree{{op}}; + tree.add_child(std::move(t)); + tree.add_child(std::move(o2)); + return tree; + } +} + +Mu::Parser::Parser(const Store& store, Parser::Flags flags) : + priv_{std::make_unique<Private>(store, flags)} +{ +} + +Mu::Parser::~Parser() = default; + +Mu::Tree +Mu::Parser::parse(const std::string& expr, WarningVec& warnings) const +{ + try { + auto tokens = tokenize(expr); + if (tokens.empty()) + return empty(); + else + return priv_->term_1(tokens, warnings); + + } catch (const std::runtime_error& ex) { + std::cerr << ex.what() << std::endl; + return empty(); + } +} diff --git a/lib/mu-parser.hh b/lib/mu-parser.hh new file mode 100644 index 0000000..65adc64 --- /dev/null +++ b/lib/mu-parser.hh @@ -0,0 +1,106 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef __PARSER_HH__ +#define __PARSER_HH__ + +#include "utils/mu-utils.hh" +#include <string> +#include <vector> +#include <memory> + +#include <mu-tree.hh> +#include <mu-store.hh> + +// A simple recursive-descent parser for queries. Follows the Xapian syntax, +// but better handles non-alphanum; also implements regexp + +namespace Mu { + +/** + * A parser warning + * + */ +struct Warning { + size_t pos{}; /**< pos in string */ + const std::string msg; /**< warning message */ + + /** + * operator== + * + * @param rhs right-hand side + * + * @return true if rhs is equal to this; false otherwise + */ + bool operator==(const Warning& rhs) const { return pos == rhs.pos && msg == rhs.msg; } +}; +using WarningVec = std::vector<Warning>; + +/** + * operator<< + * + * @param os an output stream + * @param w a warning + * + * @return the updated output stream + */ +inline std::ostream& +operator<<(std::ostream& os, const Warning& w) +{ + os << w.pos << ":" << w.msg; + return os; +} + +class Parser { + public: + enum struct Flags { None = 0, UnitTest = 1 << 0 }; + + /** + * Construct a query parser object + * + * @param store a store object ptr, or none + */ + Parser(const Store& store, Flags = Flags::None); + /** + * DTOR + * + */ + ~Parser(); + + /** + * Parse a query string + * + * @param query a query string + * @param warnings vec to receive warnings + * + * @return a parse-tree + */ + + Tree parse(const std::string& query, WarningVec& warnings) const; + + private: + struct Private; + std::unique_ptr<Private> priv_; +}; + +MU_ENABLE_BITOPS(Parser::Flags); + +} // namespace Mu + +#endif /* __PARSER_HH__ */ diff --git a/lib/mu-query-match-deciders.cc b/lib/mu-query-match-deciders.cc new file mode 100644 index 0000000..999d609 --- /dev/null +++ b/lib/mu-query-match-deciders.cc @@ -0,0 +1,223 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-query-match-deciders.hh" + +#include "mu-query-results.hh" +#include "utils/mu-option.hh" + +using namespace Mu; + + +// We use a MatchDecider to gather information about the matches, and decide +// whether to include them in the results. +// +// Note that to include the "related" messages, we need _two_ queries; the first +// one to get the initial matches (called the Leader-Query) and a Related-Query, +// to get the Leader matches + all messages that have a thread-id seen in the +// Leader matches. +// +// We use the MatchDecider to gather information and use it for both queries. + +struct MatchDecider : public Xapian::MatchDecider { + MatchDecider(QueryFlags qflags, DeciderInfo& info) : qflags_{qflags}, decider_info_{info} {} + /** + * Update the match structure with unreadable/duplicate flags + * + * @param doc a Xapian document. + * + * @return a new QueryMatch object + */ + QueryMatch make_query_match(const Xapian::Document& doc) const + { + QueryMatch qm{}; + + auto msgid{opt_string(doc, Field::Id::MessageId) + .value_or(*opt_string(doc, Field::Id::Path))}; + if (!decider_info_.message_ids.emplace(std::move(msgid)).second) + qm.flags |= QueryMatch::Flags::Duplicate; + + const auto path{opt_string(doc, Field::Id::Path)}; + if (!path || ::access(path->c_str(), R_OK) != 0) + qm.flags |= QueryMatch::Flags::Unreadable; + + return qm; + } + + /** + * Should this message be included in the results? + * + * @param qm a query match + * + * @return true or false + */ + bool should_include(const QueryMatch& qm) const + { + if (any_of(qflags_ & QueryFlags::SkipDuplicates) && + any_of(qm.flags & QueryMatch::Flags::Duplicate)) + return false; + + if (any_of(qflags_ & QueryFlags::SkipUnreadable) && + any_of(qm.flags & QueryMatch::Flags::Unreadable)) + return false; + + return true; + } + /** + * Gather thread ids from this match. + * + * @param doc the document (message) + * + */ + void gather_thread_ids(const Xapian::Document& doc) const + { + auto thread_id{opt_string(doc, Field::Id::ThreadId)}; + if (thread_id) + decider_info_.thread_ids.emplace(std::move(*thread_id)); + } + +protected: + const QueryFlags qflags_; + DeciderInfo& decider_info_; + +private: + Option<std::string> opt_string(const Xapian::Document& doc, Field::Id id) const noexcept { + const auto value_no{field_from_id(id).value_no()}; + std::string val = xapian_try([&] { return doc.get_value(value_no); }, std::string{""}); + if (val.empty()) + return Nothing; + else + return Some(std::move(val)); + } +}; + +struct MatchDeciderLeader final : public MatchDecider { + MatchDeciderLeader(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {} + /** + * operator() + * + * This receives the documents considered during a Xapian query, and + * is to return either true (keep) or false (ignore) + * + * We use this to potentiallly avoid certain messages (documents): + * - with QueryFlags::SkipUnreadable this will return false for message + * that are not readable in the file-system + * - with QueryFlags::SkipDuplicates this will return false for messages + * whose message-id was seen before. + * + * Even if we do not skip these messages entirely, we remember whether + * they were unreadable/duplicate (in the QueryMatch::Flags), so we can + * quickly find that info when doing the second 'related' query. + * + * The "leader" query. Matches here get the Leader flag unless they are + * duplicates / unreadable. We check the duplicate/readable status + * regardless of whether SkipDuplicates/SkipUnreadable was passed + * (to gather that information); however those flags + * affect our true/false verdict. + * + * @param doc xapian document + * + * @return true or false + */ + bool operator()(const Xapian::Document& doc) const override { + // by definition, we haven't seen the docid before, + // so no need to search + auto it = decider_info_.matches.emplace(doc.get_docid(), make_query_match(doc)); + it.first->second.flags |= QueryMatch::Flags::Leader; + + return should_include(it.first->second); + } +}; + +std::unique_ptr<Xapian::MatchDecider> +Mu::make_leader_decider(QueryFlags qflags, DeciderInfo& info) +{ + return std::make_unique<MatchDeciderLeader>(qflags, info); +} + +struct MatchDeciderRelated final : public MatchDecider { + MatchDeciderRelated(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {} + /** + * operator() + * + * This receives the documents considered during a Xapian query, and + * is to return either true (keep) or false (ignore) + * + * We use this to potentially avoid certain messages (documents): + * - with QueryFlags::SkipUnreadable this will return false for message + * that are not readable in the file-system + * - with QueryFlags::SkipDuplicates this will return false for messages + * whose message-id was seen before. + * + * Unlike in the "leader" decider (scroll up), we don't need to remember + * messages we won't include. + * + * @param doc xapian document + * + * @return true or false + */ + bool operator()(const Xapian::Document& doc) const override { + // we may have seen this match in the "Leader" query. + const auto it = decider_info_.matches.find(doc.get_docid()); + if (it != decider_info_.matches.end()) + return should_include(it->second); + + auto qm{make_query_match(doc)}; + if (should_include(qm)) { + qm.flags |= QueryMatch::Flags::Related; + decider_info_.matches.emplace(doc.get_docid(), std::move(qm)); + return true; + } else + return false; // nope. + } +}; + +std::unique_ptr<Xapian::MatchDecider> +Mu::make_related_decider(QueryFlags qflags, DeciderInfo& info) +{ + return std::make_unique<MatchDeciderRelated>(qflags, info); +} + +struct MatchDeciderThread final : public MatchDecider { + MatchDeciderThread(QueryFlags qflags, DeciderInfo& info) : MatchDecider{qflags, info} {} + /** + * operator() + * + * This receives the documents considered during a Xapian query, and + * is to return either true (keep) or false (ignore) + * + * Only include documents that earlier checks have decided to include. + * + * @param doc xapian document + * + * @return true or false + */ + bool operator()(const Xapian::Document& doc) const override { + // we may have seen this match in the "Leader" query, + // or in the second (unbuounded) related query; + const auto it{decider_info_.matches.find(doc.get_docid())}; + return it != decider_info_.matches.end() && !it->second.thread_path.empty(); + } +}; + +std::unique_ptr<Xapian::MatchDecider> +Mu::make_thread_decider(QueryFlags qflags, DeciderInfo& info) +{ + return std::make_unique<MatchDeciderThread>(qflags, info); +} diff --git a/lib/mu-query-match-deciders.hh b/lib/mu-query-match-deciders.hh new file mode 100644 index 0000000..91488a5 --- /dev/null +++ b/lib/mu-query-match-deciders.hh @@ -0,0 +1,76 @@ +/* +** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_QUERY_MATCH_DECIDERS_HH__ +#define MU_QUERY_MATCH_DECIDERS_HH__ + +#include <unordered_set> +#include <unordered_map> +#include <memory> + +#include <xapian.h> + +#include "mu-query-results.hh" + +namespace Mu { +using StringSet = std::unordered_set<std::string>; + +struct DeciderInfo { + QueryMatches matches; + StringSet thread_ids; + StringSet message_ids; +}; + +/** + * Make a "leader" decider, that is, a MatchDecider for either a singular or the + * first query in the leader/related pair of queries. Gather information for + * threading, and the subsequent "related" query. + * + * @param qflags query flags + * @param match_info receives information about the matches. + * + * @return a unique_ptr to a match decider. + */ +std::unique_ptr<Xapian::MatchDecider> make_leader_decider(QueryFlags qflags, DeciderInfo& info); + +/** + * Make a "related" decider, that is, a MatchDecider for the second query + * in the leader/related pair of queries. + * + * @param qflags query flags + * @param match_info receives information about the matches. + * + * @return a unique_ptr to a match decider. + */ +std::unique_ptr<Xapian::MatchDecider> make_related_decider(QueryFlags qflags, DeciderInfo& info); + +/** + * Make a "thread" decider, that is, a MatchDecider that removes all but the + * document excepts for the ones found during initial/related searches. + * + * @param qflags query flags + * @param match_info receives information about the matches. + * + * @return a unique_ptr to a match decider. + */ +std::unique_ptr<Xapian::MatchDecider> make_thread_decider(QueryFlags qflags, DeciderInfo& info); + +} // namespace Mu + +#endif /* MU_QUERY_MATCH_DECIDERS_HH__ */ diff --git a/lib/mu-query-results.hh b/lib/mu-query-results.hh new file mode 100644 index 0000000..7b8a72e --- /dev/null +++ b/lib/mu-query-results.hh @@ -0,0 +1,413 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_QUERY_RESULTS_HH__ +#define MU_QUERY_RESULTS_HH__ + +#include <algorithm> +#include <limits> +#include <stdexcept> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <limits> +#include <ostream> +#include <cmath> +#include <memory> + +#include <unistd.h> +#include <fcntl.h> +#include <xapian.h> +#include <glib.h> + +#include <utils/mu-utils.hh> +#include <utils/mu-option.hh> +#include <utils/mu-xapian-utils.hh> + +#include <message/mu-message.hh> + +namespace Mu { + +/** + * This implements a QueryResults structure, which capture the results of a + * Xapian query, and a QueryResultsIterator, which gives C++-compliant iterator + * to go over the results. and finally QueryThreader (in query-threader.cc) which + * calculates the threads, using the JWZ algorithm. + */ + +/// Flags that influence now matches are presented (or skipped) +enum struct QueryFlags { + None = 0, /**< no flags */ + Descending = 1 << 0, /**< sort z->a */ + SkipUnreadable = 1 << 1, /**< skip unreadable msgs */ + SkipDuplicates = 1 << 2, /**< skip duplicate msgs */ + IncludeRelated = 1 << 3, /**< include related msgs */ + Threading = 1 << 4, /**< calculate threading info */ + // internal + Leader = 1 << 5, /**< This is the leader query (for internal use + * only)*/ +}; +MU_ENABLE_BITOPS(QueryFlags); + +/// Stores all the essential information for sorting the results. +struct QueryMatch { + /// Flags for a match (message) found + enum struct Flags { + None = 0, /**< No Flags */ + Leader = 1 << 0, /**< Mark direct matches as leader */ + Related = 1 << 1, /**< A related message */ + Unreadable = 1 << 2, /**< No readable file */ + Duplicate = 1 << 3, /**< Message-id seen before */ + + Root = 1 << 10, /**< Is this the thread-root? */ + First = 1 << 11, /**< Is this the first message in a thread? */ + Last = 1 << 12, /**< Is this the last message in a thread? */ + Orphan = 1 << 13, /**< Is this message without a parent? */ + HasChild = 1 << 14, /**< Does this message have a child? */ + + ThreadSubject = 1 << 20, /**< Message holds subject for (sub)thread */ + }; + + Flags flags{Flags::None}; /**< Flags */ + std::string date_key; /**< The date-key (for sorting all sub-root levels) */ + // the thread subject is the subject of the first message in a thread, + // and any message that has a different subject compared to its predecessor + // (ignoring prefixes such as Re:) + // + // otherwise, it is empty. + std::string subject; /**< subject for this message */ + size_t thread_level{}; /**< The thread level */ + std::string thread_path; /**< The hex-numerial path in the thread, ie. '00:01:0a' */ + std::string thread_date; /**< date of newest message in thread */ + + bool operator<(const QueryMatch& rhs) const { return date_key < rhs.date_key; } + + bool has_flag(Flags flag) const; +}; + +MU_ENABLE_BITOPS(QueryMatch::Flags); + +inline bool +QueryMatch::has_flag(QueryMatch::Flags flag) const +{ + return any_of(flags & flag); +} + +inline std::ostream& +operator<<(std::ostream& os, QueryMatch::Flags mflags) +{ + if (mflags == QueryMatch::Flags::None) { + os << "<none>"; + return os; + } + + if (any_of(mflags & QueryMatch::Flags::Leader)) + os << "leader "; + if (any_of(mflags & QueryMatch::Flags::Unreadable)) + os << "unreadable "; + if (any_of(mflags & QueryMatch::Flags::Duplicate)) + os << "dup "; + + if (any_of(mflags & QueryMatch::Flags::Root)) + os << "root "; + if (any_of(mflags & QueryMatch::Flags::Related)) + os << "related "; + if (any_of(mflags & QueryMatch::Flags::First)) + os << "first "; + if (any_of(mflags & QueryMatch::Flags::Last)) + os << "last "; + if (any_of(mflags & QueryMatch::Flags::Orphan)) + os << "orphan "; + if (any_of(mflags & QueryMatch::Flags::HasChild)) + os << "has-child "; + + return os; +} + +using QueryMatches = std::unordered_map<Xapian::docid, QueryMatch>; + +inline std::ostream& +operator<<(std::ostream& os, const QueryMatch& qmatch) +{ + os << "qm:[" << qmatch.thread_path << "]: " // " (" << qmatch.thread_level << "): " + << "> date:<" << qmatch.date_key << "> " + << "flags:{" << qmatch.flags << "}"; + + return os; +} + +/// +/// This is a view over the Xapian::MSet, which can optionally filter unreadable +/// / duplicate messages. +/// +/// Note, we internally skip unreadable/duplicate messages (when asked too); those +/// skipped ones do _not_ count towards the max_size +/// +class QueryResultsIterator { +public: + using iterator_category = std::output_iterator_tag; + using value_type = Message; + using difference_type = void; + using pointer = void; + using reference = void; + + QueryResultsIterator(Xapian::MSetIterator mset_it, QueryMatches& query_matches) + : mset_it_{mset_it}, query_matches_{query_matches} { + } + + /** + * Increment the iterator (we don't support post-increment) + * + * @return an updated iterator, or end() if we were already at end() + */ + QueryResultsIterator& operator++() { + ++mset_it_; + mdoc_ = Nothing; + return *this; + } + + /** + * (Non)Equivalence operators + * + * @param rhs some other iterator + * + * @return true or false + */ + bool operator==(const QueryResultsIterator& rhs) const { return mset_it_ == rhs.mset_it_; } + bool operator!=(const QueryResultsIterator& rhs) const { return mset_it_ != rhs.mset_it_; } + + QueryResultsIterator& operator*() { return *this; } + const QueryResultsIterator& operator*() const { return *this; } + + + /** + * Get the Xapian::Document this iterator is pointing at, + * or an empty document when looking at end(). + * + * @return a document + */ + Option<Xapian::Document> document() const { + return xapian_try([this]()->Option<Xapian::Document> { + auto doc{mset_it_.get_document()}; + if (doc.get_docid() == 0) + return Nothing; + else + return Some(std::move(doc)); + }, Nothing); + } + + + /** + * get the corresponding Message for this iter, if any + * + * @return a Message or Nothing + */ + Option<Message> message() const { + if (auto&& xdoc{document()}; !xdoc) + return Nothing; + else if (auto&& doc{Message::make_from_document(std::move(xdoc.value()))}; + !doc) + return Nothing; + else + return Some(std::move(doc.value())); + } + + /** + * Get the doc-id for the document this iterator is pointing at, or 0 + * when looking at end. + * + * @return a doc-id. + */ + Xapian::docid doc_id() const { return *mset_it_; } + + /** + * Get the message-id for the document (message) this iterator is + * pointing at, or not when not available + * + * @return a message-id + */ + Option<std::string> message_id() const noexcept { + return opt_string(Field::Id::MessageId); + } + + /** + * Get the thread-id for the document (message) this iterator is + * pointing at, or Nothing. + * + * @return a message-id + */ + Option<std::string> thread_id() const noexcept { + return opt_string(Field::Id::ThreadId); + } + + /** + * Get the file-system path for the document (message) this iterator is + * pointing at, or Nothing. + * + * @return a filesystem path + */ + Option<std::string> path() const noexcept { + return opt_string(Field::Id::Path); + } + + /** + * Get the a sortable date str for the document (message) the iterator + * is pointing at. pointing at, or Nothing. This (encoded) string + * has the same sort-order as the corresponding date. + * + * @return a filesystem path + */ + Option<std::string> date_str() const noexcept { + return opt_string(Field::Id::Date); + } + + /** + * Get the subject for the document (message) this iterator is pointing + * at. + * + * @return the subject + */ + Option<std::string> subject() const noexcept { + return opt_string(Field::Id::Subject); + } + + /** + * Get the references for the document (messages) this is iterator is + * pointing at, or empty if pointing at end of if no references are + * available. + * + * @return references + */ + std::vector<std::string> references() const noexcept { + return mu_document().string_vec_value(Field::Id::References); + } + + /** + * Get some value from the document, or Nothing if empty. + * + * @param id a message field id + * + * @return the value + */ + Option<std::string> opt_string(Field::Id id) const noexcept { + if (auto&& val{mu_document().string_value(id)}; val.empty()) + return Nothing; + else + return Some(std::move(val)); + } + + /** + * Get the Query match info for this message. + * + * @return the match info. + */ + QueryMatch& query_match() { + g_assert(query_matches_.find(doc_id()) != query_matches_.end()); + return query_matches_.find(doc_id())->second; + } + const QueryMatch& query_match() const { + g_assert(query_matches_.find(doc_id()) != query_matches_.end()); + return query_matches_.find(doc_id())->second; + } + +private: + /** + * Get a (cached) reference for the Mu::Document corresponding + * to the current iter. + * + * @return cached mu document, + */ + const Mu::Document& mu_document() const { + if (!mdoc_) { + if (auto xdoc = document(); !xdoc) + std::runtime_error("iter without document"); + else + mdoc_ = Mu::Document{xdoc.value()}; + } + return mdoc_.value(); + } + + mutable Option<Mu::Document> mdoc_; // cache. + Xapian::MSetIterator mset_it_; + QueryMatches& query_matches_; +}; + +constexpr auto MaxQueryResultsSize = std::numeric_limits<size_t>::max(); + +class QueryResults { +public: + /// Helper types + using iterator = QueryResultsIterator; + using const_iterator = const iterator; + + /** + * Construct a QueryResults object + * + * @param mset an Xapian::MSet with matches + */ + QueryResults(const Xapian::MSet& mset, QueryMatches&& query_matches) + : mset_{mset}, query_matches_{std::move(query_matches)} + { + } + /** + * Is this QueryResults object empty (ie., no matches)? + * + * @return true are false + */ + bool empty() const { return mset_.empty(); } + + /** + * Get the number of matches in this QueryResult + * + * @return number of matches + */ + size_t size() const { return mset_.size(); } + + /** + * Get the begin iterator to the results. + * + * @return iterator + */ + const iterator begin() const { return QueryResultsIterator(mset_.begin(), query_matches_); } + + /** + * Get the end iterator to the results. + * + * @return iterator + */ + const_iterator end() const { return QueryResultsIterator(mset_.end(), query_matches_); } + + /** + * Get the query-matches for these QueryResults. The non-const + * version can be use to _steal_ the query results, by moving + * them. + * + * @return query-matches + */ + const QueryMatches& query_matches() const { return query_matches_; } + QueryMatches& query_matches() { return query_matches_; } + +private: + const Xapian::MSet mset_; + mutable QueryMatches query_matches_; +}; + +} // namespace Mu + +#endif /* MU_QUERY_RESULTS_HH__ */ diff --git a/lib/mu-query-threads.cc b/lib/mu-query-threads.cc new file mode 100644 index 0000000..22a3b7a --- /dev/null +++ b/lib/mu-query-threads.cc @@ -0,0 +1,919 @@ +/* +** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-query-threads.hh" +#include <message/mu-message.hh> + +#include <set> +#include <unordered_set> +#include <list> +#include <cassert> +#include <cstring> +#include <iostream> +#include <iomanip> + +#include <utils/mu-option.hh> + +using namespace Mu; + +struct Container { + using Containers = std::vector<Container*>; + + Container() = default; + Container(Option<QueryMatch&> msg) : query_match{msg} {} + Container(const Container&) = delete; + Container(Container&&) = default; + + void add_child(Container& new_child) + { + new_child.parent = this; + children.emplace_back(&new_child); + } + void remove_child(Container& child) + { + children.erase(find_child(child)); + assert(!has_child(child)); + } + + Containers::iterator find_child(Container& child) + { + return std::find_if(children.begin(), children.end(), [&](auto&& c) { + return c == &child; + }); + } + Containers::const_iterator find_child(Container& child) const + { + return std::find_if(children.begin(), children.end(), [&](auto&& c) { + return c == &child; + }); + } + bool has_child(Container& child) const { return find_child(child) != children.cend(); } + + bool is_reachable(Container* other) const + { + auto up{ur_parent()}; + return up && up == other->ur_parent(); + } + template <typename Func> void for_each_child(Func&& func) + { + auto it{children.rbegin()}; + while (it != children.rend()) { + auto next = std::next(it); + func(*it); + it = next; + } + } + // During sorting, this is the cached value for the (recursive) date-key + // of this container -- ie.. either the one from the first of its + // children, or from its query-match, if it has no children. + // + // Note that the sub-root-levels of threads are always sorted by date, + // in ascending order, regardless of whatever sorting was specified for + // the root-level. + + std::string thread_date_key; + + Option<QueryMatch&> query_match; + bool is_nuked{}; + Container* parent{}; + Containers children; + + using ContainerVec = std::vector<Container*>; + + private: + const Container* ur_parent() const + { + assert(this->parent != this); + return parent ? parent->ur_parent() : this; + } +}; + +using Containers = Container::Containers; +using ContainerVec = Container::ContainerVec; + +static std::ostream& +operator<<(std::ostream& os, const Container& container) +{ + os << "container: " << std::right << std::setw(10) << &container + << ": parent: " << std::right << std::setw(10) << container.parent << " [" + << container.thread_date_key << "]" + << "\n children: "; + + for (auto&& c : container.children) + os << std::right << std::setw(10) << c << " "; + + os << (container.is_nuked ? " nuked" : ""); + + if (container.query_match) + os << "\n " << container.query_match.value(); + + return os; +} + +using IdTable = std::unordered_map<std::string, Container>; +using DupTable = std::multimap<std::string, Container>; + +static void +handle_duplicates(IdTable& id_table, DupTable& dup_table) +{ + size_t n{}; + + for (auto&& dup : dup_table) { + const auto msgid{dup.first}; + auto it = id_table.find(msgid); + if (it == id_table.end()) + continue; + + // add duplicates as fake children + char buf[32]; + ::snprintf(buf, sizeof(buf), "dup-%zu", ++n); + it->second.add_child(id_table.emplace(buf, std::move(dup.second)).first->second); + } +} + +template <typename QueryResultsType> +static IdTable +determine_id_table(QueryResultsType& qres) +{ + // 1. For each query_match + IdTable id_table; + DupTable dups; + for (auto&& mi : qres) { + const auto msgid{mi.message_id().value_or(*mi.path())}; + // Step 0 (non-JWZ): filter out dups, handle those at the end + if (mi.query_match().has_flag(QueryMatch::Flags::Duplicate)) { + dups.emplace(msgid, mi.query_match()); + continue; + } + // 1.A If id_table contains an empty Container for this ID: + // Store this query_match (query_match) in the Container's query_match (value) slot. + // Else: + // Create a new Container object holding this query_match (query-match); + // Index the Container by Query_Match-ID + auto c_it = id_table.find(msgid); + auto& container = [&]() -> Container& { + if (c_it != id_table.end()) { + if (!c_it->second.query_match) // hmm, dup? + c_it->second.query_match = mi.query_match(); + return c_it->second; + } else { + // Else: + // Create a new Container object holding this query_match + // (query-match); Index the Container by Query_Match-ID + return id_table.emplace(msgid, mi.query_match()).first->second; + } + }(); + + // We sort by date (ascending), *except* for the root; we don't + // know what query_matchs will be at the root level yet, so remember + // both. Moreover, even when sorting the top-level in descending + // order, still sort the thread levels below that in ascending + // order. + container.thread_date_key = container.query_match->date_key = + mi.date_str().value_or(""); + // initial guess for the thread-date; might be updated + // later. + + // remember the subject, we use it to determine the (sub)thread subject + container.query_match->subject = mi.subject().value_or(""); + + // 1.B + // For each element in the query_match's References field: + Container* parent_ref_container{}; + for (const auto& ref : mi.references()) { + // grand_<n>-parent -> grand_<n-1>-parent -> ... -> parent. + + // Find a Container object for the given Query_Match-ID; If it exists, use + // it; otherwise make one with a null Query_Match. + auto ref_container = [&]() -> Container* { + auto ref_it = id_table.find(ref); + if (ref_it == id_table.end()) + ref_it = id_table.emplace(ref, Nothing).first; + return &ref_it->second; + }(); + + // Link the References field's Containers together in the order implied + // by the References header. + // * If they are already linked, don't change the existing links. + // + // * Do not add a link if adding that link would introduce a loop: that is, + // before asserting A->B, search down the children of B to see if A is + // reachable, and also search down the children of A to see if B is + // reachable. If either is already reachable as a child of the other, + // don't add the link. + if (parent_ref_container && !ref_container->parent) { + if (!parent_ref_container->is_reachable(ref_container)) + parent_ref_container->add_child(*ref_container); + // else + // g_message ("%u: reachable %s -> %s", __LINE__, + // msgid.c_str(), ref.c_str()); + } + + parent_ref_container = ref_container; + } + + // Add the query_match to the chain. + if (parent_ref_container && !container.parent) { + if (!parent_ref_container->is_reachable(&container)) + parent_ref_container->add_child(container); + // else + // g_message ("%u: reachable %s -> parent", __LINE__, + // msgid.c_str()); + } + } + + // non-JWZ: add duplicate messages. + handle_duplicates(id_table, dups); + + return id_table; +} + +/// Recursively walk all containers under the root set. +/// For each container: +/// +/// If it is an empty container with no children, nuke it. +/// +/// Note: Normally such containers won't occur, but they can show up when two +/// query_matchs have References lines that disagree. For example, assuming A and +/// B are query_matchs, and 1, 2, and 3 are references for query_matchs we haven't +/// seen: +/// +/// A has references: 1, 2, 3 +/// B has references: 1, 3 +/// +/// There is ambiguity as to whether 3 is a child of 1 or of 2. So, +/// depending on the processing order, we might end up with either +/// +/// -- 1 +/// |-- 2 +/// \-- 3 +/// |-- A +/// \-- B +/// +/// or +/// +/// -- 1 +/// |-- 2 <--- non root childless container! +/// \-- 3 +/// |-- A +/// \-- B +/// +/// If the Container has no Query_Match, but does have children, remove this +/// container but promote its children to this level (that is, splice them in +/// to the current child list.) +/// +/// Do not promote the children if doing so would promote them to the root +/// set -- unless there is only one child, in which case, do. + +static void +prune(Container* child) +{ + Container* container{child->parent}; + + for (auto& grandchild : child->children) { + grandchild->parent = container; + if (container) + container->children.emplace_back(grandchild); + } + + child->children.clear(); + child->is_nuked = true; + + if (container) + container->remove_child(*child); +} + +static bool +prune_empty_containers(Container& container) +{ + Containers to_prune; + + container.for_each_child([&](auto& child) { + if (prune_empty_containers(*child)) + to_prune.emplace_back(child); + }); + + for (auto& child : to_prune) + prune(child); + + // Never nuke these. + if (container.query_match) + return false; + + // If it is an empty container with no children, nuke it. + // + // If the Container is empty, but does have children, remove this + // container but promote its children to this level (that is, splice them in + // to the current child list.) + // + // Do not promote the children if doing so would promote them to the root + // set -- unless there is only one child, in which case, do. + // const auto rootset_child{!container.parent->parent}; + if (container.parent || container.children.size() <= 1) + return true; // splice/nuke it. + + return false; +} + +static void +prune_empty_containers(IdTable& id_table) +{ + for (auto&& item : id_table) { + auto& child(item.second); + if (child.parent) + continue; // not a root child. + + if (prune_empty_containers(item.second)) + prune(&child); + } +} + +// +// Sorting. +// + +/// Register some information about a match (i.e., message) that we can use for +/// subsequent queries. +using ThreadPath = std::vector<unsigned>; +inline std::string +to_string(const ThreadPath& tpath, size_t digits) +{ + std::string str; + str.reserve(tpath.size() * digits); + + bool first{true}; + for (auto&& segm : tpath) { + str += format("%s%0*x", first ? "" : ":", (int)digits, segm); + first = false; + } + + return str; +} + +static bool // compare subjects, ignore anything before the last ':<space>*' +subject_matches(const std::string& sub1, const std::string& sub2) +{ + auto search_str = [](const std::string& s) -> const char* { + const auto pos = s.find_last_of(':'); + if (pos == std::string::npos) + return s.c_str(); + else { + const auto pos2 = s.find_first_not_of(' ', pos + 1); + return s.c_str() + (pos2 == std::string::npos ? pos : pos2); + } + }; + + // g_debug ("'%s' '%s'", search_str(sub1), search_str(sub2)); + return g_strcmp0(search_str(sub1), search_str(sub2)) == 0; +} + +static bool +update_container(Container& container, + bool descending, + ThreadPath& tpath, + size_t seg_size, + const std::string& prev_subject = "") +{ + if (!container.children.empty()) { + Container* first = container.children.front(); + if (first->query_match) + first->query_match->flags |= QueryMatch::Flags::First; + Container* last = container.children.back(); + if (last->query_match) + last->query_match->flags |= QueryMatch::Flags::Last; + } + + if (!container.query_match) + return false; // nothing else to do. + + auto& qmatch(*container.query_match); + if (!container.parent) + qmatch.flags |= QueryMatch::Flags::Root; + else if (!container.parent->query_match) + qmatch.flags |= QueryMatch::Flags::Orphan; + + if (!container.children.empty()) + qmatch.flags |= QueryMatch::Flags::HasChild; + + if (qmatch.has_flag(QueryMatch::Flags::Root) || prev_subject.empty() || + !subject_matches(prev_subject, qmatch.subject)) + qmatch.flags |= QueryMatch::Flags::ThreadSubject; + + if (descending && container.parent) { + // trick xapian by giving it "inverse" sorting key so our + // ascending-date sorted threads stay in that order + tpath.back() = ((1U << (4 * seg_size)) - 1) - tpath.back(); + } + + qmatch.thread_path = to_string(tpath, seg_size); + qmatch.thread_level = tpath.size() - 1; + + // ensure thread root comes before its children + if (descending) + qmatch.thread_path += ":z"; + + return true; +} + +static void +update_containers(Containers& children, + bool descending, + ThreadPath& tpath, + size_t seg_size, + std::string& prev_subject) +{ + size_t idx{0}; + + for (auto&& c : children) { + tpath.emplace_back(idx++); + if (c->query_match) { + update_container(*c, descending, tpath, seg_size, prev_subject); + prev_subject = c->query_match->subject; + } + update_containers(c->children, descending, tpath, seg_size, prev_subject); + tpath.pop_back(); + } +} + +static void +update_containers(ContainerVec& root_vec, bool descending, size_t n) +{ + ThreadPath tpath; + tpath.reserve(n); + + const auto seg_size = static_cast<size_t>(std::ceil(std::log2(n) / 4.0)); + /*note: 4 == std::log2(16)*/ + + size_t idx{0}; + for (auto&& c : root_vec) { + tpath.emplace_back(idx++); + std::string prev_subject; + if (update_container(*c, descending, tpath, seg_size)) + prev_subject = c->query_match->subject; + update_containers(c->children, descending, tpath, seg_size, prev_subject); + tpath.pop_back(); + } +} + +static void +sort_container(Container& container) +{ + // 1. childless container. + if (container.children.empty()) + return; // no children; nothing to sort. + + // 2. container with children. + // recurse, depth-first: sort the children + for (auto& child : container.children) + sort_container(*child); + + // now sort this level. + std::sort(container.children.begin(), container.children.end(), [&](auto&& c1, auto&& c2) { + return c1->thread_date_key < c2->thread_date_key; + }); + + // and 'bubble up' the date of the *newest* message with a date. We + // reasonably assume that it's later than its parent. + const auto& newest_date = container.children.back()->thread_date_key; + if (!newest_date.empty()) + container.thread_date_key = newest_date; +} + +static void +sort_siblings(IdTable& id_table, bool descending) +{ + if (id_table.empty()) + return; + + // unsorted vec of root containers. We can + // only sort these _after_ sorting the children. + ContainerVec root_vec; + for (auto&& item : id_table) { + if (!item.second.parent && !item.second.is_nuked) + root_vec.emplace_back(&item.second); + } + + // now sort all threads _under_ the root set (by date/ascending) + for (auto&& c : root_vec) + sort_container(*c); + + // and then sort the root set. + // + // The difference with the sub-root containers is that at the top-level, + // we can sort either in ascending or descending order, while on the + // subroot level it's always in ascending order. + // + // Note that unless we're testing, _xapian_ will handle + // the ascending/descending of the top level. + std::sort(root_vec.begin(), root_vec.end(), [&](auto&& c1, auto&& c2) { +#ifdef BUILD_TESTS + if (descending) + return c2->thread_date_key < c1->thread_date_key; + else +#endif /*BUILD_TESTS*/ + return c1->thread_date_key < c2->thread_date_key; + }); + + // now all is sorted... final step is to determine thread paths and + // other flags. + update_containers(root_vec, descending, id_table.size()); +} + +static std::ostream& +operator<<(std::ostream& os, const IdTable& id_table) +{ + os << "------------------------------------------------\n"; + for (auto&& item : id_table) { + os << item.first << " => " << item.second << "\n"; + } + os << "------------------------------------------------\n"; + + std::set<std::string> ids; + for (auto&& item : id_table) { + if (item.second.query_match) + ids.emplace(item.second.query_match->thread_path); + } + + for (auto&& id : ids) { + auto it = std::find_if(id_table.begin(), id_table.end(), [&](auto&& item) { + return item.second.query_match && + item.second.query_match->thread_path == id; + }); + assert(it != id_table.end()); + os << it->first << ": " << it->second << '\n'; + } + return os; +} + +template <typename Results> +static void +calculate_threads_real(Results& qres, bool descending) +{ + // Step 1: build the id_table + auto id_table{determine_id_table(qres)}; + + if (g_test_verbose()) + std::cout << "*** id-table(1):\n" << id_table << "\n"; + + // // Step 2: get the root set + // // Step 3: discard id_table + // Nope: id-table owns the containers. + // Step 4: prune empty containers + prune_empty_containers(id_table); + + // Step 5: group root-set by subject. + // Not implemented. + + // Step 6: we're done threading + + // Step 7: sort siblings. The segment-size is the number of hex-digits + // in the thread-path string (so we can lexically compare them.) + sort_siblings(id_table, descending); + + // Step 7a:. update querymatches + for (auto&& item : id_table) { + Container& c{item.second}; + if (c.query_match) + c.query_match->thread_date = c.thread_date_key; + } + // if (g_test_verbose()) + // std::cout << "*** id-table(2):\n" << id_table << "\n"; +} + +void +Mu::calculate_threads(Mu::QueryResults& qres, bool descending) +{ + calculate_threads_real(qres, descending); +} + +#ifdef BUILD_TESTS + +struct MockQueryResult { + MockQueryResult(const std::string& message_id_arg, + const std::string& date_arg, + const std::vector<std::string>& refs_arg = {}) + : message_id_{message_id_arg}, date_{date_arg}, refs_{refs_arg} + { + } + MockQueryResult(const std::string& message_id_arg, + const std::vector<std::string>& refs_arg = {}) + : MockQueryResult(message_id_arg, "", refs_arg) + { + } + Option<std::string> message_id() const { return message_id_; } + Option<std::string> path() const { return path_; } + Option<std::string> date_str() const { return date_; } + Option<std::string> subject() const { return subject_; } + QueryMatch& query_match() { return query_match_; } + const QueryMatch& query_match() const { return query_match_; } + const std::vector<std::string>& references() const { return refs_; } + + std::string path_; + std::string message_id_; + QueryMatch query_match_{}; + std::string date_; + std::string subject_; + std::vector<std::string> refs_; +}; + +using MockQueryResults = std::vector<MockQueryResult>; + +G_GNUC_UNUSED static std::ostream& +operator<<(std::ostream& os, const MockQueryResults& qrs) +{ + for (auto&& mi : qrs) + os << mi.query_match().thread_path << " :: " << mi.message_id().value_or("<none>") + << std::endl; + + return os; +} + +static void +calculate_threads(MockQueryResults& qres, bool descending) +{ + calculate_threads_real(qres, descending); +} + +using Expected = std::vector<std::pair<std::string, std::string>>; + +static void +assert_thread_paths(const MockQueryResults& qrs, const Expected& expected) +{ + for (auto&& exp : expected) { + auto it = std::find_if(qrs.begin(), qrs.end(), [&](auto&& qr) { + return qr.message_id().value_or("") == exp.first || + qr.path().value_or("") == exp.first; + }); + g_assert_true(it != qrs.end()); + g_assert_cmpstr(exp.second.c_str(), ==, it->query_match().thread_path.c_str()); + } +} + +static void +test_sort_ascending() +{ + auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}}, + MockQueryResult{"m2", "2", {"m3"}}, + MockQueryResult{"m3", "3", {}}, + MockQueryResult{"m4", "4", {}}}; + + calculate_threads(results, false); + + assert_thread_paths(results, {{"m1", "0:0:0"}, {"m2", "0:0"}, {"m3", "0"}, {"m4", "1"}}); +} + +static void +test_sort_descending() +{ + auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}}, + MockQueryResult{"m2", "2", {"m3"}}, + MockQueryResult{"m3", "3", {}}, + MockQueryResult{"m4", "4", {}}}; + + calculate_threads(results, true); + + assert_thread_paths(results, + {{"m1", "1:f:f:z"}, {"m2", "1:f:z"}, {"m3", "1:z"}, {"m4", "0:z"}}); +} + +static void +test_id_table_inconsistent() +{ + auto results = MockQueryResults{ + MockQueryResult{"m1", "1", {"m2"}}, // 1->2 + MockQueryResult{"m2", "2", {"m1"}}, // 2->1 + MockQueryResult{"m3", "3", {"m3"}}, // self ref + MockQueryResult{"m4", "4", {"m3", "m5"}}, + MockQueryResult{"m5", "5", {"m4", "m4"}}, // dup parent + }; + + calculate_threads(results, false); + assert_thread_paths(results, + { + {"m2", "0"}, + {"m1", "0:0"}, + {"m3", "1"}, + {"m5", "1:0"}, + {"m4", "1:0:0"}, + }); +} + +static void +test_dups_dup_last() +{ + MockQueryResult r1{"m1", "1", {}}; + r1.query_match().flags |= QueryMatch::Flags::Leader; + r1.path_ = "/path1"; + + MockQueryResult r1_dup{"m1", "1", {}}; + r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate; + r1_dup.path_ = "/path2"; + + auto results = MockQueryResults{r1, r1_dup}; + + calculate_threads(results, false); + + assert_thread_paths(results, + { + {"/path1", "0"}, + {"/path2", "0:0"}, + }); +} + +static void +test_dups_dup_first() +{ + // now dup becomes the leader; this will _demote_ + // r1. + + MockQueryResult r1_dup{"m1", "1", {}}; + r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate; + r1_dup.path_ = "/path1"; + + MockQueryResult r1{"m1", "1", {}}; + r1.query_match().flags |= QueryMatch::Flags::Leader; + r1.path_ = "/path2"; + + auto results = MockQueryResults{r1_dup, r1}; + + calculate_threads(results, false); + + assert_thread_paths(results, + { + {"/path2", "0"}, + {"/path1", "0:0"}, + }); +} + +static void +test_do_not_prune_root_empty_with_children() +{ + // m7 should not be nuked + auto results = MockQueryResults{ + MockQueryResult{"x1", "1", {"m7"}}, + MockQueryResult{"x2", "2", {"m7"}}, + }; + + calculate_threads(results, false); + + assert_thread_paths(results, + { + {"x1", "0:0"}, + {"x2", "0:1"}, + }); +} + +static void +test_prune_root_empty_with_child() +{ + // m7 should be nuked + auto results = MockQueryResults{ + MockQueryResult{"m1", "1", {"m7"}}, + }; + + calculate_threads(results, false); + + assert_thread_paths(results, + { + {"m1", "0"}, + }); +} + +static void +test_prune_empty_with_children() +{ + // m6 should be nuked + auto results = MockQueryResults{ + MockQueryResult{"m1", "1", {"m7", "m6"}}, + MockQueryResult{"m2", "2", {"m7", "m6"}}, + }; + + calculate_threads(results, false); + + assert_thread_paths(results, + { + {"m1", "0:0"}, + {"m2", "0:1"}, + }); +} + +static void +test_thread_info_ascending() +{ + auto results = MockQueryResults{ + MockQueryResult{"m1", "5", {}}, + MockQueryResult{"m2", "1", {}}, + MockQueryResult{"m3", "3", {"m2"}}, + MockQueryResult{"m4", "2", {"m2"}}, + // orphan siblings + MockQueryResult{"m10", "6", {"m9"}}, + MockQueryResult{"m11", "7", {"m9"}}, + }; + calculate_threads(results, false); + + assert_thread_paths(results, + { + {"m2", "0"}, // 2 + {"m4", "0:0"}, // 2 + {"m3", "0:1"}, // 3 + {"m1", "1"}, // 5 + + {"m10", "2:0"}, // 6 + {"m11", "2:1"}, // 7 + }); + + g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root)); + g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root | + QueryMatch::Flags::HasChild)); + g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last)); + g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First)); + g_assert_true(results[4].query_match().has_flag(QueryMatch::Flags::Orphan | + QueryMatch::Flags::First)); + g_assert_true( + results[5].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last)); +} + +static void +test_thread_info_descending() +{ + auto results = MockQueryResults{ + MockQueryResult{"m1", "5", {}}, + MockQueryResult{"m2", "1", {}}, + MockQueryResult{"m3", "3", {"m2"}}, + MockQueryResult{"m4", "2", {"m2"}}, + // orphan siblings + MockQueryResult{"m10", "6", {"m9"}}, + MockQueryResult{"m11", "7", {"m9"}}, + }; + calculate_threads(results, true /*descending*/); + + assert_thread_paths(results, + { + {"m1", "1:z"}, // 5 + {"m2", "2:z"}, // 2 + {"m4", "2:f:z"}, // 2 + {"m3", "2:e:z"}, // 3 + + {"m10", "0:f:z"}, // 6 + {"m11", "0:e:z"}, // 7 + }); + g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root)); + g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root | + QueryMatch::Flags::HasChild)); + g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last)); + g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First)); + + g_assert_true( + results[4].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last)); + g_assert_true(results[5].query_match().has_flag(QueryMatch::Flags::Orphan | + QueryMatch::Flags::First)); +} + +int +main(int argc, char* argv[]) +try { + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/threader/sort/ascending", test_sort_ascending); + g_test_add_func("/threader/sort/decending", test_sort_descending); + + g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent); + g_test_add_func("/threader/dups/dup-last", test_dups_dup_last); + g_test_add_func("/threader/dups/dup-first", test_dups_dup_first); + + g_test_add_func("/threader/prune/do-not-prune-root-empty-with-children", + test_do_not_prune_root_empty_with_children); + g_test_add_func("/threader/prune/prune-root-empty-with-child", + test_prune_root_empty_with_child); + g_test_add_func("/threader/prune/prune-empty-with-children", + test_prune_empty_with_children); + + g_test_add_func("/threader/thread-info/ascending", test_thread_info_ascending); + g_test_add_func("/threader/thread-info/descending", test_thread_info_descending); + + return g_test_run(); +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} catch (...) { + std::cerr << "caught exception\n"; + return 1; +} + +#endif /*BUILD_TESTS*/ diff --git a/lib/mu-query-threads.hh b/lib/mu-query-threads.hh new file mode 100644 index 0000000..5aab888 --- /dev/null +++ b/lib/mu-query-threads.hh @@ -0,0 +1,41 @@ +/* +** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_QUERY_THREADS__ +#define MU_QUERY_THREADS__ + +#include "mu-query-results.hh" + +namespace Mu { +/** + * Calculate the threads for these query results; that is, determine the + * thread-paths for each message, so we can let Xapian order them in the correct + * order. + * + * Note - threads are sorted chronologically, and the messages below the top + * level are always sorted in ascending orde + * + * @param qres query results + * @param descending whether to sort the top-level in descending order + */ +void calculate_threads(QueryResults& qres, bool descending); + +} // namespace Mu + +#endif /*MU_QUERY_THREADS__*/ diff --git a/lib/mu-query.cc b/lib/mu-query.cc new file mode 100644 index 0000000..8be9052 --- /dev/null +++ b/lib/mu-query.cc @@ -0,0 +1,306 @@ +/* +** Copyright (C) 2008-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include <mu-query.hh> + +#include <stdexcept> +#include <string> +#include <cctype> +#include <cstring> +#include <sstream> +#include <cmath> + +#include <stdlib.h> +#include <xapian.h> +#include <glib/gstdio.h> + +#include "mu-query-results.hh" +#include "mu-query-match-deciders.hh" +#include "mu-query-threads.hh" +#include <mu-xapian.hh> +#include "utils/mu-xapian-utils.hh" + +using namespace Mu; + +struct Query::Private { + Private(const Store& store) : store_{store}, parser_{store_} {} + // New + // bool calculate_threads (Xapian::Enquire& enq, size maxnum); + + Xapian::Enquire make_enquire(const std::string& expr, Field::Id sortfield_id, + QueryFlags qflags) const; + Xapian::Enquire make_related_enquire(const StringSet& thread_ids, + Field::Id sortfield_id, + QueryFlags qflags) const; + + Option<QueryResults> run_threaded(QueryResults&& qres, Xapian::Enquire& enq, + QueryFlags qflags, size_t max_size) const; + Option<QueryResults> run_singular(const std::string& expr, + Field::Id sortfield_id, + QueryFlags qflags, size_t maxnum) const; + Option<QueryResults> run_related(const std::string& expr, + Field::Id sortfield_id, + QueryFlags qflags, size_t maxnum) const; + + Option<QueryResults> run(const std::string& expr, + Field::Id sortfield_id, QueryFlags qflags, + size_t maxnum) const; + + size_t store_size() const { return store_.database().get_doccount(); } + + const Store& store_; + const Parser parser_; +}; + +Query::Query(const Store& store) : priv_{std::make_unique<Private>(store)} {} + +Query::Query(Query&& other) = default; + +Query::~Query() = default; + +static Xapian::Enquire& +sort_enquire(Xapian::Enquire& enq, Field::Id sortfield_id, QueryFlags qflags) +{ + const auto value_no{field_from_id(sortfield_id).value_no()}; + enq.set_sort_by_value(value_no, any_of(qflags & QueryFlags::Descending)); + + return enq; +} + +Xapian::Enquire +Query::Private::make_enquire(const std::string& expr, + Field::Id sortfield_id, + QueryFlags qflags) const +{ + Xapian::Enquire enq{store_.database()}; + + if (expr.empty() || expr == R"("")") + enq.set_query(Xapian::Query::MatchAll); + else { + WarningVec warns; + const auto tree{parser_.parse(expr, warns)}; + for (auto&& w : warns) + g_warning("query warning: %s", to_string(w).c_str()); + enq.set_query(xapian_query(tree)); + g_debug("qtree: %s", to_string(tree).c_str()); + } + + sort_enquire(enq, sortfield_id, qflags); + + return enq; +} + +Xapian::Enquire +Query::Private::make_related_enquire(const StringSet& thread_ids, + Field::Id sortfield_id, + QueryFlags qflags) const +{ + Xapian::Enquire enq{store_.database()}; + std::vector<Xapian::Query> qvec; + for (auto&& t : thread_ids) + qvec.emplace_back(field_from_id(Field::Id::ThreadId).xapian_term(t)); + + Xapian::Query qr{Xapian::Query::OP_OR, qvec.begin(), qvec.end()}; + enq.set_query(qr); + + sort_enquire(enq, sortfield_id, qflags); + + return enq; +} + +struct ThreadKeyMaker : public Xapian::KeyMaker { + ThreadKeyMaker(const QueryMatches& matches) : match_info_(matches) {} + std::string operator()(const Xapian::Document& doc) const override + { + const auto it{match_info_.find(doc.get_docid())}; + return (it == match_info_.end()) ? "" : it->second.thread_path; + } + const QueryMatches& match_info_; +}; + +Option<QueryResults> +Query::Private::run_threaded(QueryResults&& qres, Xapian::Enquire& enq, QueryFlags qflags, + size_t maxnum) const +{ + const auto descending{any_of(qflags & QueryFlags::Descending)}; + + calculate_threads(qres, descending); + + ThreadKeyMaker key_maker{qres.query_matches()}; + enq.set_sort_by_key(&key_maker, descending); + + DeciderInfo minfo; + minfo.matches = qres.query_matches(); + auto mset{enq.get_mset(0, maxnum, {}, make_thread_decider(qflags, minfo).get())}; + mset.fetch(); + + return QueryResults{mset, std::move(qres.query_matches())}; +} + +Option<QueryResults> +Query::Private::run_singular(const std::string& expr, + Field::Id sortfield_id, + QueryFlags qflags, size_t maxnum) const +{ + // i.e. a query _without_ related messages, but still possibly + // with threading. + // + // In the threading case, the sortfield-id is ignored, we always sort by + // date (since threading the threading results are always by date.) + + const auto singular_qflags{qflags | QueryFlags::Leader}; + const auto threading{any_of(qflags & QueryFlags::Threading)}; + + DeciderInfo minfo{}; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wextra" + auto enq{make_enquire(expr, threading ? Field::Id::Date : sortfield_id, qflags)}; +#pragma GCC diagnostic ignored "-Wswitch-default" +#pragma GCC diagnostic pop + auto mset{enq.get_mset(0, maxnum, {}, + make_leader_decider(singular_qflags, minfo).get())}; + mset.fetch(); + + auto qres{QueryResults{mset, std::move(minfo.matches)}}; + + return threading ? run_threaded(std::move(qres), enq, qflags, maxnum) : qres; +} + +static Option<std::string> +opt_string(const Xapian::Document& doc, Field::Id id) noexcept +{ + const auto value_no{field_from_id(id).value_no()}; + std::string val = + xapian_try([&] { return doc.get_value(value_no); }, std::string{""}); + if (val.empty()) + return Nothing; + else + return Some(std::move(val)); +} + +Option<QueryResults> +Query::Private::run_related(const std::string& expr, + Field::Id sortfield_id, + QueryFlags qflags, size_t maxnum) const +{ + // i.e. a query _with_ related messages and possibly with threading. + // + // In the threading case, the sortfield-id is ignored, we always sort by + // date (since threading the threading results are always by date.); + // moreover, in either threaded or non-threaded case, we sort the first + // ("leader") query by date, i.e, we prefer the newest or oldest + // (descending) messages. + const auto leader_qflags{QueryFlags::Leader | qflags}; + const auto threading{any_of(qflags & QueryFlags::Threading)}; + + // Run our first, "leader" query + DeciderInfo minfo{}; + auto enq{make_enquire(expr, Field::Id::Date, leader_qflags)}; + const auto mset{ + enq.get_mset(0, maxnum, {}, make_leader_decider(leader_qflags, minfo).get())}; + + // Gather the thread-ids we found + mset.fetch(); + for (auto it = mset.begin(); it != mset.end(); ++it) { + auto thread_id{opt_string(it.get_document(), Field::Id::ThreadId)}; + if (thread_id) + minfo.thread_ids.emplace(std::move(*thread_id)); + } + + // Now, determine the "related query". + // + // In the threaded-case, we search among _all_ messages, since complete + // threads are preferred; no need to sort in that case since the search + // is unlimited and the sorting happens during threading. + auto r_enq = std::invoke([&]{ + if (threading) + return make_related_enquire(minfo.thread_ids, Field::Id::Date, + qflags ); + else + return make_related_enquire(minfo.thread_ids, sortfield_id, qflags); + }); + + const auto r_mset{r_enq.get_mset(0, threading ? store_size() : maxnum, {}, + make_related_decider(qflags, minfo).get())}; + auto qres{QueryResults{r_mset, std::move(minfo.matches)}}; + return threading ? run_threaded(std::move(qres), r_enq, qflags, maxnum) : qres; +} + +Option<QueryResults> +Query::Private::run(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, + size_t maxnum) const +{ + const auto eff_maxnum{maxnum == 0 ? store_size() : maxnum}; + + if (any_of(qflags & QueryFlags::IncludeRelated)) + return run_related(expr, sortfield_id, qflags, eff_maxnum); + else + return run_singular(expr, sortfield_id, qflags, eff_maxnum); +} + +Result<QueryResults> +Query::run(const std::string& expr, Field::Id sortfield_id, + QueryFlags qflags, size_t maxnum) const +{ + // some flags are for internal use only. + g_return_val_if_fail(none_of(qflags & QueryFlags::Leader), + Err(Error::Code::InvalidArgument, "cannot pass Leader flag")); + + StopWatch sw{format( + "ran query '%s'; related: %s; threads: %s; max-size: %zu", expr.c_str(), + any_of(qflags & QueryFlags::IncludeRelated) ? "yes" : "no", + any_of(qflags & QueryFlags::Threading) ? "yes" : "no", maxnum)}; + + return xapian_try_result([&]{ + if (auto&& res = priv_->run(expr, sortfield_id, qflags, maxnum); res) + return Result<QueryResults>(Ok(std::move(res.value()))); + else + return Result<QueryResults>(Err(Error::Code::Query, + "failed to run query")); + }); + +} + +size_t +Query::count(const std::string& expr) const +{ + return xapian_try( + [&] { + const auto enq{priv_->make_enquire(expr, {}, {})}; + auto mset{enq.get_mset(0, priv_->store_size())}; + mset.fetch(); + return mset.size(); + }, + 0); +} + +/* LCOV_EXCL_START*/ +std::string +Query::parse(const std::string& expr, bool xapian) const +{ + WarningVec warns; + const auto tree{priv_->parser_.parse(expr, warns)}; + for (auto&& w : warns) + g_warning("query warning: %s", to_string(w).c_str()); + + if (xapian) + return xapian_query(tree).get_description(); + else + return to_string(tree); +} +/* LCOV_EXCL_STOP*/ diff --git a/lib/mu-query.hh b/lib/mu-query.hh new file mode 100644 index 0000000..ad042e1 --- /dev/null +++ b/lib/mu-query.hh @@ -0,0 +1,101 @@ +/* +** Copyright (C) 2008-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_QUERY_HH__ +#define __MU_QUERY_HH__ + +#include <memory> + +#include <glib.h> +#include <mu-store.hh> +#include <mu-query-results.hh> +#include <utils/mu-utils.hh> +#include <utils/mu-option.hh> +#include <utils/mu-result.hh> +#include <message/mu-message.hh> + +namespace Mu { + +class Query { +public: + /** + * Run a query on the store + * + * @param expr the search expression + * @param sortfield_id the sortfield-id. Default to Date + * @param flags query flags + * @param maxnum maximum number of results to return. 0 for 'no limit' + * + * @return the query-results or an error + */ + Result<QueryResults> run(const std::string& expr, + Field::Id sortfield_id = Field::Id::Date, + QueryFlags flags = QueryFlags::None, + size_t maxnum = 0) const; + + /** + * run a Xapian query to count the number of matches; for the syntax, please + * refer to the mu-query manpage + * + * @param expr the search expression; use "" to match all messages + * + * @return the number of matches + */ + size_t count(const std::string& expr = "") const; + + /** + * For debugging, get the internal string representation of the parsed + * query + * + * @param expr a xapian search expression + * @param xapian if true, show Xapian's internal representation, + * otherwise, mu's. + + * @return the string representation of the query + */ + std::string parse(const std::string& expr, bool xapian) const; + +private: + friend class Store; + + /** + * Construct a new Query instance. + * + * @param store a MuStore object + */ + Query(const Store& store); + /** + * DTOR + * + */ + ~Query(); + + /** + * Move CTOR + * + * @param other + */ + Query(Query&& other); + + struct Private; + std::unique_ptr<Private> priv_; +}; +} // namespace Mu + +#endif /*__MU_QUERY_HH__*/ diff --git a/lib/mu-runtime.cc b/lib/mu-runtime.cc new file mode 100644 index 0000000..b3b0e60 --- /dev/null +++ b/lib/mu-runtime.cc @@ -0,0 +1,117 @@ +/* +** Copyright (C) 2019-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-runtime.hh" +#include "utils/mu-util.h" +#include "utils/mu-logger.hh" + +#include <locale.h> /* for setlocale() */ + +#include <string> +#include <unordered_map> +static std::unordered_map<MuRuntimePath, std::string> RuntimePaths; + +constexpr auto PartsDir = "parts"; +constexpr auto LogDir = "log"; +constexpr auto XapianDir = "xapian"; +constexpr auto MuName = "mu"; +constexpr auto Bookmarks = "bookmarks"; + +static const std::string Sepa{G_DIR_SEPARATOR_S}; + +static void +init_paths_xdg() +{ + RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB, + g_get_user_cache_dir() + Sepa + MuName + Sepa + XapianDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, g_get_user_cache_dir() + Sepa + MuName); + RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE, + g_get_user_cache_dir() + Sepa + MuName + Sepa + PartsDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, g_get_user_cache_dir() + Sepa + MuName); + RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, g_get_user_config_dir() + Sepa + MuName); +} + +static void +init_paths_muhome(const char* muhome) +{ + RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB, muhome + Sepa + XapianDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, muhome); + RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE, muhome + Sepa + PartsDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, muhome + Sepa + LogDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, muhome + Sepa + Bookmarks); +} + +gboolean +mu_runtime_init(const char* muhome, const char* name, gboolean debug) +{ + g_return_val_if_fail(RuntimePaths.empty(), FALSE); + g_return_val_if_fail(name, FALSE); + + setlocale(LC_ALL, ""); + + if (muhome) + init_paths_muhome(muhome); + else + init_paths_xdg(); + + for (const auto& d : RuntimePaths) { + char* dir; + if (d.first == MU_RUNTIME_PATH_BOOKMARKS) // special case + dir = g_path_get_dirname(d.second.c_str()); + else + dir = g_strdup(d.second.c_str()); + + auto ok = mu_util_create_dir_maybe(dir, 0700, TRUE); + if (!ok) { + g_critical("failed to create %s", dir); + g_free(dir); + mu_runtime_uninit(); + return FALSE; + } + g_free(dir); + } + + const auto log_path = RuntimePaths[MU_RUNTIME_PATH_LOGDIR] + Sepa + name + ".log"; + + using namespace Mu; + LogOptions opts{LogOptions::None}; + if (debug) + opts |= (LogOptions::Debug | LogOptions::None); + + Mu::log_init(log_path, opts); + + return TRUE; +} + +void +mu_runtime_uninit(void) +{ + RuntimePaths.clear(); + Mu::log_uninit(); +} + +const char* +mu_runtime_path(MuRuntimePath path) +{ + const auto it = RuntimePaths.find(path); + if (it == RuntimePaths.end()) + return NULL; + else + return it->second.c_str(); +} diff --git a/lib/mu-runtime.hh b/lib/mu-runtime.hh new file mode 100644 index 0000000..0f0d410 --- /dev/null +++ b/lib/mu-runtime.hh @@ -0,0 +1,66 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- +** +** Copyright (C) 2012-2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#ifndef __MU_RUNTIME_H__ +#define __MU_RUNTIME_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +/** + * initialize the mu runtime system; initializes logging and other + * systems. To uninitialize, use mu_runtime_uninit + * + * @param muhome path where to find the mu home directory (typically, ~/.cache/mu) + * @param name of the main program, ie. 'mu', 'mug' or + * 'procmule'. this influences the name of the e.g. the logfile + * @param debug debug-mode + * + * @return TRUE if succeeded, FALSE in case of error + */ +gboolean mu_runtime_init(const char* muhome, const char* name, gboolean debug); + +/** + * free all resources + * + */ +void mu_runtime_uninit(void); + +typedef enum { + MU_RUNTIME_PATH_XAPIANDB, /* mu xapian db path */ + MU_RUNTIME_PATH_BOOKMARKS, /* mu bookmarks file path */ + MU_RUNTIME_PATH_CACHE, /* mu cache path */ + MU_RUNTIME_PATH_MIMECACHE, /* mu cache path for attachments etc. */ + MU_RUNTIME_PATH_LOGDIR, /* mu path for log files */ + + MU_RUNTIME_PATH_NUM +} MuRuntimePath; + +/** + * get a file system path to some 'special' file or directory + * + * @return ma string which should be not be modified/freed, or NULL in + * case of error. + */ +const char* mu_runtime_path(MuRuntimePath path); + +G_END_DECLS + +#endif /*__MU_RUNTIME_H__*/ diff --git a/lib/mu-script.cc b/lib/mu-script.cc new file mode 100644 index 0000000..05fe3dc --- /dev/null +++ b/lib/mu-script.cc @@ -0,0 +1,375 @@ +/* +** Copyright (C) 2012-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#ifdef BUILD_GUILE + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wredundant-decls" +#include <libguile.h> +#pragma GCC diagnostic pop +#endif /*BUILD_GUILE*/ + +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <dirent.h> +#include <errno.h> +#include <unistd.h> + +#include "mu-script.hh" +#include "utils/mu-util.h" + +/** + * Structure with information about a certain script. + * the values will be *freed* when MuScriptInfo is freed + */ +struct MuScriptInfo { + char* _name; /* filename-sans-extension */ + char* _path; /* full path to script */ + char* _oneline; /* one-line description */ + char* _descr; /* longer description */ +}; + +/* create a new MuScriptInfo* object*/ +static MuScriptInfo* +script_info_new(void) +{ + return g_slice_new0(MuScriptInfo); +} + +/* destroy a MuScriptInfo* object */ +static void +script_info_destroy(MuScriptInfo* msi) +{ + if (!msi) + return; + + g_free(msi->_name); + g_free(msi->_path); + g_free(msi->_oneline); + g_free(msi->_descr); + + g_slice_free(MuScriptInfo, msi); +} + +/* compare two MuScripInfo* objects (for sorting) */ +static int +script_info_cmp(MuScriptInfo* msi1, MuScriptInfo* msi2) +{ + return strcmp(msi1->_name, msi2->_name); +} + +const char* +mu_script_info_name(MuScriptInfo* msi) +{ + g_return_val_if_fail(msi, NULL); + return msi->_name; +} + +const char* +mu_script_info_path(MuScriptInfo* msi) +{ + g_return_val_if_fail(msi, NULL); + return msi->_path; +} + +const char* +mu_script_info_one_line(MuScriptInfo* msi) +{ + g_return_val_if_fail(msi, NULL); + return msi->_oneline; +} + +const char* +mu_script_info_description(MuScriptInfo* msi) +{ + g_return_val_if_fail(msi, NULL); + return msi->_descr; +} + +gboolean +mu_script_info_matches_regex(MuScriptInfo* msi, const char* rxstr, GError** err) +{ + GRegex* rx; + gboolean match; + + g_return_val_if_fail(msi, FALSE); + g_return_val_if_fail(rxstr, FALSE); + + rx = g_regex_new(rxstr, + (GRegexCompileFlags)(G_REGEX_CASELESS | G_REGEX_OPTIMIZE), + (GRegexMatchFlags)0, + err); + if (!rx) + return FALSE; + + match = FALSE; + if (msi->_name) + match = g_regex_match(rx, msi->_name, (GRegexMatchFlags)0, NULL); + if (!match && msi->_oneline) + match = g_regex_match(rx, msi->_oneline, (GRegexMatchFlags)0, NULL); + + return match; +} + +void +mu_script_info_list_destroy(GSList* lst) +{ + g_slist_free_full(lst, (GDestroyNotify)script_info_destroy); +} + +static GIOChannel* +open_channel(const char* path) +{ + GError* err; + GIOChannel* io_chan; + + err = NULL; + + io_chan = g_io_channel_new_file(path, "r", &err); + if (!io_chan) { + g_warning("failed to open '%s': %s", + path, + err ? err->message : "something went wrong"); + g_clear_error(&err); + return NULL; + } + + return io_chan; +} + +static void +end_channel(GIOChannel* io_chan) +{ + GIOStatus status; + GError* err; + + err = NULL; + status = g_io_channel_shutdown(io_chan, FALSE, &err); + if (status != G_IO_STATUS_NORMAL) { + g_warning("failed to shutdown io-channel: %s", + err ? err->message : "something went wrong"); + g_clear_error(&err); + } + + g_io_channel_unref(io_chan); +} + +static gboolean +get_descriptions(MuScriptInfo* msi, const char* prefix) +{ + GIOStatus io_status; + GIOChannel* script_io; + GError* err; + + char *line, *descr, *oneline; + + if (!prefix) + return TRUE; /* not an error */ + + if (!(script_io = open_channel(msi->_path))) + return FALSE; + + err = NULL; + line = descr = oneline = NULL; + + do { + g_free(line); + io_status = g_io_channel_read_line(script_io, &line, NULL, NULL, &err); + if (io_status != G_IO_STATUS_NORMAL) + break; + + if (!g_str_has_prefix(line, prefix)) + continue; + + if (!oneline) + oneline = g_strdup(line + strlen(prefix)); + else { + char* tmp; + tmp = descr; + descr = g_strdup_printf("%s%s", descr ? descr : "", line + strlen(prefix)); + g_free(tmp); + } + + } while (TRUE); + + if (io_status != G_IO_STATUS_EOF) { + g_warning("error reading %s: %s", + msi->_path, + err ? err->message : "something went wrong"); + g_clear_error(&err); + } + + end_channel(script_io); + msi->_oneline = oneline; + msi->_descr = descr; + + return TRUE; +} + +GSList* +mu_script_get_script_info_list(const char* path, + const char* ext, + const char* descprefix, + GError** err) +{ + DIR* dir; + GSList* lst; + struct dirent* dentry; + + g_return_val_if_fail(path, NULL); + + dir = opendir(path); + if (!dir) { + mu_util_g_set_error(err, + MU_ERROR_FILE_CANNOT_OPEN, + "failed to open '%s': %s", + path, + g_strerror(errno)); + return NULL; + } + + /* create a list of names, paths */ + lst = NULL; + while ((dentry = readdir(dir))) { + MuScriptInfo* msi; + /* only consider files with certain extensions, + * if ext != NULL */ + if (ext && !g_str_has_suffix(dentry->d_name, ext)) + continue; + msi = script_info_new(); + msi->_name = g_strdup(dentry->d_name); + if (ext) /* strip the extension */ + msi->_name[strlen(msi->_name) - strlen(ext)] = '\0'; + msi->_path = g_strdup_printf("%s%c%s", path, G_DIR_SEPARATOR, dentry->d_name); + /* set the one-line and long description */ + get_descriptions(msi, descprefix); + lst = g_slist_prepend(lst, msi); + } + + closedir(dir); /* ignore error checking... */ + + return g_slist_sort(lst, (GCompareFunc)script_info_cmp); +} + +MuScriptInfo* +mu_script_find_script_with_name(GSList* lst, const char* name) +{ + GSList* cur; + + g_return_val_if_fail(name, NULL); + + for (cur = lst; cur; cur = g_slist_next(cur)) { + MuScriptInfo* msi; + msi = (MuScriptInfo*)cur->data; + + if (g_strcmp0(name, mu_script_info_name(msi)) == 0) + return msi; + } + + return NULL; +} + + + +#ifdef BUILD_GUILE +static char* +quoted_from_strv (const gchar **params) +{ + GString *str; + int i; + + g_return_val_if_fail (params, NULL); + + if (!params[0]) + return g_strdup (""); + + str = g_string_sized_new (64); /* just a guess */ + + for (i = 0; params[i]; ++i) { + + if (i > 0) + g_string_append_c (str, ' '); + + g_string_append_c (str, '"'); + g_string_append (str, params[i]); + g_string_append_c (str, '"'); + } + + return g_string_free (str, FALSE); +} + + +static void +guile_shell(void* closure, int argc, char** argv) +{ + scm_shell(argc, argv); +} + +gboolean +mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err) +{ + const char* s; + char * mainargs, *expr; + char** argv; + + g_return_val_if_fail(msi, FALSE); + g_return_val_if_fail(muhome, FALSE); + + if (access(mu_script_info_path(msi), R_OK) != 0) { + mu_util_g_set_error(err, + MU_ERROR_FILE_CANNOT_READ, + "failed to read script: %s", + g_strerror(errno)); + return FALSE; + } + + argv = g_new0(char*, 6); + argv[0] = g_strdup(GUILE_BINARY); + argv[1] = g_strdup("-l"); + + s = mu_script_info_path(msi); + argv[2] = g_strdup(s ? s : ""); + + mainargs = quoted_from_strv(args); + expr = g_strdup_printf("(main '(\"%s\" \"--muhome=%s\" %s))", + mu_script_info_name(msi), + muhome, + mainargs ? mainargs : ""); + + g_free(mainargs); + argv[3] = g_strdup("-c"); + argv[4] = expr; + + scm_boot_guile(5, argv, guile_shell, NULL); + + /* never reached but let's be correct(TM)*/ + g_strfreev(argv); + return TRUE; +} +#else /*!BUILD_GUILE*/ +gboolean +mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err) +{ + mu_util_g_set_error(err, MU_ERROR_INTERNAL, "this mu does not have guile support"); + return FALSE; +} +#endif /*!BUILD_GUILE*/ diff --git a/lib/mu-script.hh b/lib/mu-script.hh new file mode 100644 index 0000000..d3f7b1e --- /dev/null +++ b/lib/mu-script.hh @@ -0,0 +1,125 @@ +/* +** Copyright (C) 2012-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_SCRIPT_HH__ +#define MU_SCRIPT_HH__ + +#include <glib.h> + +/* Opaque structure with information about a script */ +struct MuScriptInfo; + +/** + * get the name of the script (sans-extension, if some extension was + * provided to mu_script_get_scripts) + * + * @param msi a MuScriptInfo structure + * + * @return the name + */ +const char* mu_script_info_name(MuScriptInfo* msi); + +/** + * get the full filesystem path of the script + * + * @param msi a MuScriptInfo structure + * + * @return the path + */ +const char* mu_script_info_path(MuScriptInfo* msi); + +/** + * get a one-line description for the script + * + * @param msi a MuScriptInfo structure + * + * @return the description, or NULL if there was none + */ +const char* mu_script_info_one_line(MuScriptInfo* msi); + +/** + * get a full description for the script + * + * @param msi a MuScriptInfo structure + * + * @return the description, or NULL if there was none + */ +const char* mu_script_info_description(MuScriptInfo* msi); + +/** + * check whether either the name or one-line description of a + * MuScriptInfo matches regular expression rxstr + * + * @param msi a MuScriptInfo + * @param rxstr a regular expression string + * @param err receives error information + * + * @return TRUE if it matches, FALSE if not or in case of error + */ +gboolean mu_script_info_matches_regex(MuScriptInfo* msi, const char* rxstr, GError** err); + +/** + * Get the list of all scripts in path with extension ext + * + * @param path a file system path + * @param ext an extension (e.g., ".scm"), or NULL + * @param prefix for the one-line description + * (e.g., ";; DESCRIPTION: "), or NULL + * @param err receives error information, if any + * + * @return a list of Mu + */ +GSList* mu_script_get_script_info_list(const char* path, + const char* ext, + const char* descprefix, + GError** err); + +/** + * destroy a list of MuScriptInfo* objects + * + * @param scriptslst a list of MuScriptInfo* objects + */ +void mu_script_info_list_destroy(GSList* lst); + +/** + * find the MuScriptInfo object for the first script with a certain + * name, or return NULL if not found. + * + * @param lst a list of MuScriptInfo* objects + * @param name the name to search for + * + * @return a MuScriptInfo* object, or NULL if not found. + */ +MuScriptInfo* mu_script_find_script_with_name(GSList* lst, const char* name); + +/** + * run the guile script at path + * + * @param msi MuScriptInfo object for the script + * @param muhome path to the mu home dir + * @param args NULL-terminated array of strings (argv for the script) + * @param err receives error information + * + * @return FALSE in case of error -- otherwise, this function will + * _not return_ + */ +gboolean +mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err); + +#endif /*MU_SCRIPT_HH__*/ diff --git a/lib/mu-server.cc b/lib/mu-server.cc new file mode 100644 index 0000000..e1f62d3 --- /dev/null +++ b/lib/mu-server.cc @@ -0,0 +1,1106 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include "message/mu-message.hh" +#include "mu-server.hh" + +#include <iostream> +#include <string> +#include <algorithm> +#include <atomic> +#include <thread> +#include <mutex> +#include <functional> + +#include <cstring> +#include <glib.h> +#include <glib/gprintf.h> + +#include "mu-runtime.hh" +#include "mu-maildir.hh" +#include "mu-query.hh" +#include "index/mu-indexer.hh" +#include "mu-store.hh" + +#include "utils/mu-utils.hh" +#include "utils/mu-option.hh" +#include "utils/mu-command-parser.hh" +#include "utils/mu-readline.hh" + +using namespace Mu; +using namespace Command; + +/// @brief object to manage the server-context for all commands. +struct Server::Private { + Private(Store& store, Output output) + : store_{store}, output_{output}, command_map_{make_command_map()}, + keep_going_{true} + {} + + ~Private() { + indexer().stop(); + if (index_thread_.joinable()) + index_thread_.join(); + } + // + // construction helpers + // + CommandMap make_command_map(); + + // + // acccessors + Store& store() { return store_; } + const Store& store() const { return store_; } + Indexer& indexer() { return store().indexer(); } + const CommandMap& command_map() const { return command_map_; } + + // + // invoke + // + bool invoke(const std::string& expr) noexcept; + + // + // output + // + void output_sexp(Sexp&& sexp,Server::OutputFlags flags = {}) const { + if (output_) + output_(std::move(sexp), flags); + } + + void output_sexp(Sexp::List&& lst, Server::OutputFlags flags = {}) const { + output_sexp(Sexp::make_list(std::move(lst)), flags); + } + size_t output_results(const QueryResults& qres, size_t batch_size) const; + + // + // handlers for various commands. + // + void add_handler(const Parameters& params); + void compose_handler(const Parameters& params); + void contacts_handler(const Parameters& params); + void find_handler(const Parameters& params); + void help_handler(const Parameters& params); + void index_handler(const Parameters& params); + void move_handler(const Parameters& params); + void mkdir_handler(const Parameters& params); + void ping_handler(const Parameters& params); + void quit_handler(const Parameters& params); + void remove_handler(const Parameters& params); + void sent_handler(const Parameters& params); + void view_handler(const Parameters& params); + +private: + // helpers + Sexp build_message_sexp(const Message& msg, + Store::Id docid, + const Option<QueryMatch&> qm) const; + + Sexp::List move_docid(Store::Id docid, Option<std::string> flagstr, + bool new_name, bool no_view); + + Sexp::List perform_move(Store::Id docid, + const Message& msg, + const std::string& maildirarg, + Flags flags, + bool new_name, + bool no_view); + + bool maybe_mark_as_read(Store::Id docid, Flags old_flags, bool rename); + bool maybe_mark_msgid_as_read(const std::string& msgid, bool rename); + + Store& store_; + Server::Output output_; + const CommandMap command_map_; + std::atomic<bool> keep_going_{}; + std::thread index_thread_; +}; + +static Sexp +build_metadata(const QueryMatch& qmatch) +{ + Sexp::List mdata; + + auto symbol_t = [] { return Sexp::make_symbol("t"); }; + + mdata.add_prop(":path", Sexp::make_string(qmatch.thread_path)); + mdata.add_prop(":level", Sexp::make_number(qmatch.thread_level)); + mdata.add_prop(":date", Sexp::make_string(qmatch.thread_date)); + + Sexp::List dlist; + const auto td{::atoi(qmatch.thread_date.c_str())}; + dlist.add(Sexp::make_number((unsigned)(td >> 16))); + dlist.add(Sexp::make_number((unsigned)(td & 0xffff))); + dlist.add(Sexp::make_number(0)); + mdata.add_prop(":date-tstamp", Sexp::make_list(std::move(dlist))); + + if (qmatch.has_flag(QueryMatch::Flags::Root)) + mdata.add_prop(":root", symbol_t()); + if (qmatch.has_flag(QueryMatch::Flags::Related)) + mdata.add_prop(":related", symbol_t()); + if (qmatch.has_flag(QueryMatch::Flags::First)) + mdata.add_prop(":first-child", symbol_t()); + if (qmatch.has_flag(QueryMatch::Flags::Last)) + mdata.add_prop(":last-child", symbol_t()); + if (qmatch.has_flag(QueryMatch::Flags::Orphan)) + mdata.add_prop(":orphan", symbol_t()); + if (qmatch.has_flag(QueryMatch::Flags::Duplicate)) + mdata.add_prop(":duplicate", symbol_t()); + if (qmatch.has_flag(QueryMatch::Flags::HasChild)) + mdata.add_prop(":has-child", symbol_t()); + if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject)) + mdata.add_prop(":thread-subject", symbol_t()); + + return Sexp::make_list(std::move(mdata)); +} + +/* + * A message here is a Sexp::List consists of a message s-expression with + * optionally a :meta expression added. + */ +Sexp +Server::Private::build_message_sexp(const Message& msg, + Store::Id docid, + const Option<QueryMatch&> qm) const +{ + auto sexp_list = msg.to_sexp_list(); + if (docid != 0) + sexp_list.add_prop(":docid", Sexp::make_number(docid)); + if (qm) + sexp_list.add_prop(":meta", build_metadata(*qm)); + + return Sexp::make_list(std::move(sexp_list)); +} + +CommandMap +Server::Private::make_command_map() +{ + CommandMap cmap; + + using Type = Sexp::Type; + + cmap.emplace( + "add", + CommandInfo{ + ArgMap{{":path", ArgInfo{Type::String, true, "file system path to the message"}}}, + "add a message to the store", + [&](const auto& params) { add_handler(params); }}); + + cmap.emplace( + "compose", + CommandInfo{ + ArgMap{ + {":type", + ArgInfo{Type::Symbol, + true, + "type of composition: reply/forward/edit/resend/new"}}, + {":docid", + ArgInfo{Type::Number, false, "document id of parent-message, if any"}}, + {":decrypt", + ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)"}}}, + "compose a new message", + [&](const auto& params) { compose_handler(params); }}); + + cmap.emplace( + "contacts", + CommandInfo{ + ArgMap{{":personal", ArgInfo{Type::Symbol, false, "only personal contacts"}}, + {":after", + ArgInfo{Type::String, false, "only contacts seen after time_t string"}}, + {":tstamp", ArgInfo{Type::String, false, "return changes since tstamp"}}, + {":maxnum", ArgInfo{Type::Number, false, "max number of contacts to return"}}}, + "get contact information", + [&](const auto& params) { contacts_handler(params); }}); + cmap.emplace( + "find", + CommandInfo{ + ArgMap{{":query", ArgInfo{Type::String, true, "search expression"}}, + {":threads", + ArgInfo{Type::Symbol, false, "whether to include threading information"}}, + {":sortfield", ArgInfo{Type::Symbol, false, "the field to sort results by"}}, + {":descending", + ArgInfo{Type::Symbol, false, "whether to sort in descending order"}}, + {":batch-size", ArgInfo{Type::Number, false, "batch size for result"}}, + {":maxnum", ArgInfo{Type::Number, false, "maximum number of result (hint)"}}, + {":skip-dups", + ArgInfo{Type::Symbol, + false, + "whether to skip messages with duplicate message-ids"}}, + {":include-related", + ArgInfo{Type::Symbol, + false, + "whether to include other message related to matching ones"}}}, + "query the database for messages", + [&](const auto& params) { find_handler(params); }}); + + cmap.emplace( + "help", + CommandInfo{ + ArgMap{{":command", ArgInfo{Type::Symbol, false, "command to get information for"}}, + {":full", ArgInfo{Type::Symbol, false, "show full descriptions"}}}, + "get information about one or all commands", + [&](const auto& params) { help_handler(params); }}); + cmap.emplace( + "index", + CommandInfo{ + ArgMap{{":my-addresses", ArgInfo{Type::List, false, "list of 'my' addresses"}}, + {":cleanup", + ArgInfo{Type::Symbol, + false, + "whether to remove stale messages from the store"}}, + {":lazy-check", + ArgInfo{Type::Symbol, + false, + "whether to avoid indexing up-to-date directories"}}}, + "scan maildir for new/updated/removed messages", + [&](const auto& params) { index_handler(params); }}); + + cmap.emplace( + "move", + CommandInfo{ + ArgMap{ + {":docid", ArgInfo{Type::Number, false, "document-id"}}, + {":msgid", ArgInfo{Type::String, false, "message-id"}}, + {":flags", ArgInfo{Type::String, false, "new flags for the message"}}, + {":maildir", ArgInfo{Type::String, false, "the target maildir"}}, + {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}}, + {":no-view", + ArgInfo{Type::Symbol, false, "if set, do not hint at updating the view"}}, + }, + "move messages and/or change their flags", + + [&](const auto& params) { move_handler(params); }}); + + cmap.emplace( + "mkdir", + CommandInfo{ + ArgMap{{":path", ArgInfo{Type::String, true, "location for the new maildir"}}}, + "create a new maildir", + [&](const auto& params) { mkdir_handler(params); }}); + cmap.emplace( + "ping", + CommandInfo{ + ArgMap{ + {":queries", + ArgInfo{Type::List, false, "queries for which to get read/unread numbers"}}, + {":skip-dups", + ArgInfo{Type::Symbol, + false, + "whether to exclude messages with duplicate message-ids"}}, + }, + "ping the mu-server and get information in response", + [&](const auto& params) { ping_handler(params); }}); + + cmap.emplace("quit", CommandInfo{{}, "quit the mu server", [&](const auto& params) { + quit_handler(params); + }}); + + cmap.emplace( + "remove", + CommandInfo{ + ArgMap{{":docid", + ArgInfo{Type::Number, true, "document-id for the message to remove"}}}, + "remove a message from filesystem and database", + [&](const auto& params) { remove_handler(params); }}); + + cmap.emplace( + "sent", + CommandInfo{ArgMap{{":path", ArgInfo{Type::String, true, "path to the message file"}}}, + "tell mu about a message that was sent", + [&](const auto& params) { sent_handler(params); }}); + + cmap.emplace( + "view", + CommandInfo{ArgMap{ + {":docid", ArgInfo{Type::Number, false, "document-id"}}, + {":msgid", ArgInfo{Type::String, false, "message-id"}}, + {":path", ArgInfo{Type::String, false, "message filesystem path"}}, + {":mark-as-read", + ArgInfo{Type::Symbol, false, "mark message as read (if not already)"}}, + {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}}, + }, + "view a message. exactly one of docid/msgid/path must be specified", + [&](const auto& params) { view_handler(params); }}); + return cmap; +} + +G_GNUC_PRINTF(2, 3) +static Sexp +make_error(Error::Code errcode, const char* frm, ...) +{ + char* msg{}; + va_list ap; + + va_start(ap, frm); + g_vasprintf(&msg, frm, ap); + va_end(ap); + + Sexp::List err; + err.add_prop(":error", Sexp::make_number(static_cast<int>(errcode))); + err.add_prop(":message", Sexp::make_string(msg)); + g_free(msg); + + return Sexp::make_list(std::move(err)); +} + +bool +Server::Private::invoke(const std::string& expr) noexcept +{ + if (!keep_going_) + return false; + + try { + auto call{Sexp::Sexp::make_parse(expr)}; + Command::invoke(command_map(), call); + + } catch (const Mu::Error& me) { + output_sexp(make_error(me.code(), "%s", me.what())); + keep_going_ = true; + } catch (const Xapian::Error& xerr) { + output_sexp(make_error(Error::Code::Internal, "xapian error: %s: %s", + xerr.get_type(), xerr.get_description().c_str())); + keep_going_ = false; + } catch (const std::runtime_error& re) { + output_sexp(make_error(Error::Code::Internal, "caught exception: %s", re.what())); + keep_going_ = false; + } catch (...) { + output_sexp(make_error(Error::Code::Internal, "something went wrong: quiting")); + keep_going_ = false; + } + + return keep_going_; +} + +/* 'add' adds a message to the database, and takes two parameters: 'path', which + * is the full path to the message, and 'maildir', which is the maildir this + * message lives in (e.g. "/inbox"). response with an (:info ...) message with + * information about the newly added message (details: see code below) + */ +void +Server::Private::add_handler(const Parameters& params) +{ + auto path{get_string_or(params, ":path")}; + const auto docid_res{store().add_message(path)}; + + if (!docid_res) + throw docid_res.error(); + + const auto docid{docid_res.value()}; + + Sexp::List expr; + expr.add_prop(":info", Sexp::make_symbol("add")); + expr.add_prop(":path", Sexp::make_string(path)); + expr.add_prop(":docid", Sexp::make_number(docid)); + + output_sexp(Sexp::make_list(std::move(expr))); + + auto msg_res{store().find_message(docid)}; + if (!msg_res) + throw Error(Error::Code::Store, + "failed to get message at %s (docid=%u)", + path.c_str(), docid); + + Sexp::List update; + update.add_prop(":update", build_message_sexp(msg_res.value(), docid, {})); + output_sexp(Sexp::make_list(std::move(update))); +} + +/* 'compose' produces the un-changed *original* message sexp (ie., the message + * to reply to, forward or edit) for a new message to compose). It takes two + * parameters: 'type' with the compose type (either reply, forward or + * edit/resend), and 'docid' for the message to reply to. Note, type:new does + * not have an original message, and therefore does not need a docid + * + * In returns a (:compose <type> [:original <original-msg>] [:include] ) + * message (detals: see code below) + * + * Note ':include' t or nil determines whether to include attachments + */ + +static Option<Sexp> +maybe_add_attachment(Message& message, const MessagePart& part, size_t index) +{ + if (!part.is_attachment()) + return Nothing; + + const auto cache_path{message.cache_path(index)}; + if (!cache_path) + throw cache_path.error(); + + const auto cooked_name{part.cooked_filename()}; + const auto fname{format("%s/%s", cache_path->c_str(), + cooked_name.value_or("part").c_str())}; + + const auto res = part.to_file(fname, true); + if (!res) + throw res.error(); + + Sexp::List pi; + + if (auto cdescr = part.content_description(); cdescr) + pi.add_prop(":description", Sexp::make_string(*cdescr)); + else if (cooked_name) + pi.add_prop(":description", Sexp::make_string(cooked_name.value())); + + pi.add_prop(":file-name", Sexp::make_string(fname)); + pi.add_prop(":mime-type", Sexp::make_string( + part.mime_type().value_or("application/octet-stream"))); + + return Some(Sexp::make_list(std::move(pi))); +} + + +void +Server::Private::compose_handler(const Parameters& params) +{ + const auto ctype{get_symbol_or(params, ":type")}; + + Sexp::List comp_lst; + comp_lst.add_prop(":compose", Sexp::make_symbol(std::string(ctype))); + + + if (ctype == "reply" || ctype == "forward" || + ctype == "edit" || ctype == "resend") { + + const unsigned docid{(unsigned)get_int_or(params, ":docid")}; + auto msg{store().find_message(docid)}; + if (!msg) + throw Error{Error::Code::Store, "failed to get message %u", docid}; + + comp_lst.add_prop(":original", build_message_sexp(msg.value(), docid, {})); + + if (ctype == "forward") { + // when forwarding, attach any attachment in the orig + size_t index{}; + Sexp::List attseq; + for (auto&& part: msg->parts()) { + if (auto attsexp = maybe_add_attachment( + *msg, part, index); attsexp) { + attseq.add(std::move(*attsexp)); + ++index; + } + } + if (!attseq.empty()) { + comp_lst.add_prop(":include", + Sexp::make_list(std::move(attseq))); + comp_lst.add_prop(":cache-path", + Sexp::make_string(*msg->cache_path())); + } + } + + } else if (ctype != "new") + throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'", + ctype.c_str()); + + output_sexp(std::move(comp_lst)); +} + +void +Server::Private::contacts_handler(const Parameters& params) +{ + const auto personal = get_bool_or(params, ":personal"); + const auto afterstr = get_string_or(params, ":after"); + const auto tstampstr = get_string_or(params, ":tstamp"); + const auto maxnum = get_int_or(params, ":maxnum", 0 /*unlimited*/); + + const auto after{afterstr.empty() ? 0 : + parse_date_time(afterstr, true).value_or(0)}; + const auto tstamp = g_ascii_strtoll(tstampstr.c_str(), NULL, 10); + + g_debug("find %s contacts last seen >= %s (tstamp: %zu)", + personal ? "personal" : "any", + time_to_string("%c", after).c_str(), + static_cast<size_t>(tstamp)); + + auto n{0}; + Sexp::List contacts; + store().contacts_cache().for_each([&](const Contact& ci) { + + /* since the last time we got some contacts */ + if (tstamp > ci.tstamp) + return true; + /* (maybe) only include 'personal' contacts */ + if (personal && !ci.personal) + return true; + /* only include newer-than-x contacts */ + if (after > ci.message_date) + return true; + + n++; + + contacts.add(Sexp::make_string(ci.display_name(true/*encode-if-needed*/))); + return maxnum == 0 || n < maxnum; + }); + + Sexp::List seq; + seq.add_prop(":contacts", Sexp::make_list(std::move(contacts))); + seq.add_prop(":tstamp", + Sexp::make_string(format("%" G_GINT64_FORMAT, + g_get_monotonic_time()))); + + /* dump the contacts cache as a giant sexp */ + g_debug("sending %d of %zu contact(s)", n, store().contacts_cache().size()); + output_sexp(std::move(seq), Server::OutputFlags::SplitList); +} + +/* get a *list* of all messages with the given message id */ +static std::vector<Store::Id> +docids_for_msgid(const Store& store, const std::string& msgid, size_t max = 100) +{ + if (msgid.size() > MaxTermLength) { + throw Error(Error::Code::InvalidArgument, + "invalid message-id '%s'", msgid.c_str()); + } else if (msgid.empty()) + return {}; + + const auto xprefix{field_from_id(Field::Id::MessageId).shortcut}; + /*XXX this is a bit dodgy */ + auto tmp{g_ascii_strdown(msgid.c_str(), -1)}; + auto expr{g_strdup_printf("%c:%s", xprefix, tmp)}; + g_free(tmp); + + GError* gerr{}; + std::lock_guard l{store.lock()}; + const auto res{store.run_query(expr, {}, QueryFlags::None, max)}; + g_free(expr); + if (!res) + throw Error(Error::Code::Store, &gerr, "failed to run msgid-query"); + else if (res->empty()) + throw Error(Error::Code::NotFound, + "could not find message(s) for msgid %s", msgid.c_str()); + + std::vector<Store::Id> docids{}; + for (auto&& mi : *res) + docids.emplace_back(mi.doc_id()); + + return docids; +} + +/* + * creating a message object just to get a path seems a bit excessive maybe + * mu_store_get_path could be added if this turns out to be a problem + */ +static std::string +path_from_docid(const Store& store, Store::Id docid) +{ + auto msg{store.find_message(docid)}; + if (!msg) + throw Error(Error::Code::Store, "could not get message from store"); + + if (auto path{msg->path()}; path.empty()) + throw Error(Error::Code::Store, "could not get path for message %u", + docid); + else + return path; +} + +static std::vector<Store::Id> +determine_docids(const Store& store, const Parameters& params) +{ + auto docid{get_int_or(params, ":docid", 0)}; + const auto msgid{get_string_or(params, ":msgid")}; + + if ((docid == 0) == msgid.empty()) + throw Error(Error::Code::InvalidArgument, + "precisely one of docid and msgid must be specified"); + + if (docid != 0) + return {static_cast<Store::Id>(docid)}; + else + return docids_for_msgid(store, msgid.c_str()); +} + +size_t +Server::Private::output_results(const QueryResults& qres, size_t batch_size) const +{ + size_t n{}; + Sexp::List headers; + + const auto output_batch = [&](Sexp::List&& hdrs) { + Sexp::List batch; + batch.add_prop(":headers", Sexp::make_list(std::move(hdrs))); + output_sexp(std::move(batch)); + }; + + for (auto&& mi : qres) { + auto msg{mi.message()}; + if (!msg) + continue; + ++n; + + // construct sexp for a single header. + auto qm{mi.query_match()}; + auto msgsexp{build_message_sexp(*msg, mi.doc_id(), qm)}; + msgsexp.formatting_opts |= Sexp::FormattingOptions::SplitList; + headers.add(std::move(msgsexp)); + // we output up-to-batch-size lists of messages. It's much + // faster (on the emacs side) to handle such batches than single + // headers. + if (headers.size() % batch_size == 0) { + output_batch(std::move(headers)); + headers.clear(); + }; + } + + // remaining. + if (!headers.empty()) + output_batch(std::move(headers)); + + return n; +} + +void +Server::Private::find_handler(const Parameters& params) +{ + const auto q{get_string_or(params, ":query")}; + const auto threads{get_bool_or(params, ":threads", false)}; + // perhaps let mu4e set this as frame-lines of the appropriate frame. + const auto batch_size{get_int_or(params, ":batch-size", 110)}; + const auto sortfieldstr{get_symbol_or(params, ":sortfield", "")}; + const auto descending{get_bool_or(params, ":descending", false)}; + const auto maxnum{get_int_or(params, ":maxnum", -1 /*unlimited*/)}; + const auto skip_dups{get_bool_or(params, ":skip-dups", false)}; + const auto include_related{get_bool_or(params, ":include-related", false)}; + + auto sort_field = std::invoke([&]()->Option<Field>{ + if (sortfieldstr.size() < 2) + return Nothing; + else + return field_from_name(sortfieldstr.substr(1)); + }); + if (!sort_field && !sortfieldstr.empty()) + throw Error{Error::Code::InvalidArgument, "invalid sort field '%s'", + sortfieldstr.c_str()}; + if (batch_size < 1) + throw Error{Error::Code::InvalidArgument, "invalid batch-size %d", batch_size}; + + auto qflags{QueryFlags::SkipUnreadable}; // don't show unreadables. + if (descending) + qflags |= QueryFlags::Descending; + if (skip_dups) + qflags |= QueryFlags::SkipDuplicates; + if (include_related) + qflags |= QueryFlags::IncludeRelated; + if (threads) + qflags |= QueryFlags::Threading; + + std::lock_guard l{store_.lock()}; + auto qres{store_.run_query(q, sort_field->id, qflags, maxnum)}; + if (!qres) + throw Error(Error::Code::Query, "failed to run query"); + + /* before sending new results, send an 'erase' message, so the frontend + * knows it should erase the headers buffer. this will ensure that the + * output of two finds will not be mixed. */ + { + Sexp::List lst; + lst.add_prop(":erase", Sexp::make_symbol("t")); + output_sexp(std::move(lst)); + } + + const auto foundnum{output_results(*qres, static_cast<size_t>(batch_size))}; + + { + Sexp::List lst; + lst.add_prop(":found", Sexp::make_number(foundnum)); + output_sexp(std::move(lst)); + } +} + +void +Server::Private::help_handler(const Parameters& params) +{ + const auto command{get_symbol_or(params, ":command", "")}; + const auto full{get_bool_or(params, ":full", !command.empty())}; + + if (command.empty()) { + std::cout << ";; Commands are s-expressions of the form\n" + << ";; (<command-name> :param1 val1 :param2 val2 ...)\n" + << ";; For instance:\n;; (help :command quit)\n" + << ";; to get detailed information about the 'quit'\n;;\n"; + std::cout << ";; The following commands are available:\n\n"; + } + + std::vector<std::string> names; + for (auto&& name_cmd : command_map()) + names.emplace_back(name_cmd.first); + std::sort(names.begin(), names.end()); + + for (auto&& name : names) { + const auto& info{command_map().find(name)->second}; + + if (!command.empty() && name != command) + continue; + + if (!command.empty()) + std::cout << ";; " + << format("%-10s -- %s\n", name.c_str(), info.docstring.c_str()); + else + std::cout << ";; " << name.c_str() << " -- " << info.docstring.c_str() + << '\n'; + if (!full) + continue; + + for (auto&& argname : info.sorted_argnames()) { + const auto& arg{info.args.find(argname)}; + std::cout << ";; " + << format("%-17s : %-24s ", + arg->first.c_str(), + to_string(arg->second).c_str()); + std::cout << " " << arg->second.docstring << "\n"; + } + std::cout << ";;\n"; + } +} + +static Sexp::List +get_stats(const Indexer::Progress& stats, const std::string& state) +{ + Sexp::List lst; + + lst.add_prop(":info", Sexp::make_symbol("index")); + lst.add_prop(":status", Sexp::make_symbol(std::string{state})); + lst.add_prop(":checked", Sexp::make_number(stats.checked)); + lst.add_prop(":updated", Sexp::make_number(stats.updated)); + lst.add_prop(":cleaned-up", Sexp::make_number(stats.removed)); + + return lst; +} + +void +Server::Private::index_handler(const Parameters& params) +{ + Mu::Indexer::Config conf{}; + conf.cleanup = get_bool_or(params, ":cleanup"); + conf.lazy_check = get_bool_or(params, ":lazy-check"); + // ignore .noupdate with an empty store. + conf.ignore_noupdate = store().empty(); + + indexer().stop(); + if (index_thread_.joinable()) + index_thread_.join(); + + // start a background track. + index_thread_ = std::thread([this, conf = std::move(conf)] { + indexer().start(conf); + while (indexer().is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + output_sexp(get_stats(indexer().progress(), "running"), + Server::OutputFlags::Flush); + } + output_sexp(get_stats(indexer().progress(), "complete"), + Server::OutputFlags::Flush); + store().commit(); /* ensure on-disk database is updated, too */ + }); +} + +void +Server::Private::mkdir_handler(const Parameters& params) +{ + const auto path{get_string_or(params, ":path")}; + if (auto&& res = maildir_mkdir(path, 0755, FALSE); !res) + throw res.error(); + + Sexp::List lst; + lst.add_prop(":info", Sexp::make_string("mkdir")); + lst.add_prop(":message", Sexp::make_string(format("%s has been created", path.c_str()))); + + output_sexp(std::move(lst)); +} + +Sexp::List +Server::Private::perform_move(Store::Id docid, + const Message& msg, + const std::string& maildirarg, + Flags flags, + bool new_name, + bool no_view) +{ + bool different_mdir{}; + auto maildir{maildirarg}; + if (maildir.empty()) { + maildir = msg.maildir(); + different_mdir = false; + } else /* are we moving to a different mdir, or is it just flags? */ + different_mdir = maildir != msg.maildir(); + + const auto new_msg = store().move_message(docid, maildir, flags, new_name); + if (!new_msg) + throw new_msg.error(); + + Sexp::List seq; + seq.add_prop(":update", build_message_sexp(new_msg.value(), docid, {})); + /* note, the :move t thing is a hint to the frontend that it + * could remove the particular header */ + if (different_mdir) + seq.add_prop(":move", Sexp::make_symbol("t")); + if (!no_view) + seq.add_prop(":maybe-view", Sexp::make_symbol("t")); + + return seq; +} + + +static Flags +calculate_message_flags(const Message& msg, Option<std::string> flagopt) +{ + const auto flags = std::invoke([&]()->Option<Flags>{ + if (!flagopt) + return msg.flags(); + else + return flags_from_expr(*flagopt, msg.flags()); + }); + + if (!flags) + throw Error{Error::Code::InvalidArgument, + "invalid flags '%s'", flagopt.value_or("").c_str()}; + else + return flags.value(); +} + +Sexp::List +Server::Private::move_docid(Store::Id docid, + Option<std::string> flagopt, + bool new_name, + bool no_view) +{ + if (docid == Store::InvalidId) + throw Error{Error::Code::InvalidArgument, "invalid docid"}; + + auto msg{store_.find_message(docid)}; + if (!msg) + throw Error{Error::Code::Store, "failed to get message from store"}; + + const auto flags = calculate_message_flags(msg.value(), flagopt); + auto lst = perform_move(docid, *msg, "", flags, new_name, no_view); + return lst; +} + +/* + * 'move' moves a message to a different maildir and/or changes its + * flags. parameters are *either* a 'docid:' or 'msgid:' pointing to + * the message, a 'maildir:' for the target maildir, and a 'flags:' + * parameter for the new flags. + * + * returns an (:update <new-msg-sexp>) + * + */ +void +Server::Private::move_handler(const Parameters& params) +{ + auto maildir{get_string_or(params, ":maildir")}; + const auto flagopt{get_string(params, ":flags")}; + const auto rename{get_bool_or(params, ":rename")}; + const auto no_view{get_bool_or(params, ":noupdate")}; + const auto docids{determine_docids(store_, params)}; + + if (docids.size() > 1) { + if (!maildir.empty()) // ie. duplicate message-ids. + throw Mu::Error{Error::Code::Store, + "can't move multiple messages at the same time"}; + // multi. + for (auto&& docid : docids) + output_sexp(move_docid(docid, flagopt, + rename, no_view)); + return; + } + auto docid{docids.at(0)}; + auto msg = store().find_message(docid) + .or_else([]{throw Error{Error::Code::InvalidArgument, + "could not create message"};}).value(); + + /* if maildir was not specified, take the current one */ + if (maildir.empty()) + maildir = msg.maildir(); + + /* determine the real target flags, which come from the flags-parameter + * we received (ie., flagstr), if any, plus the existing message + * flags. */ + const auto flags = calculate_message_flags(msg, flagopt); + output_sexp(perform_move(docid, msg, maildir, flags, rename, no_view)); +} + +void +Server::Private::ping_handler(const Parameters& params) +{ + const auto storecount{store().size()}; + if (storecount == (unsigned)-1) + throw Error{Error::Code::Store, "failed to read store"}; + + const auto queries{get_string_vec(params, ":queries")}; + Sexp::List qresults; + for (auto&& q : queries) { + const auto count{store_.count_query(q)}; + const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; + const auto unread{store_.count_query(unreadq)}; + + Sexp::List lst; + lst.add_prop(":query", Sexp::make_string(q)); + lst.add_prop(":count", Sexp::make_number(count)); + lst.add_prop(":unread", Sexp::make_number(unread)); + + qresults.add(Sexp::make_list(std::move(lst))); + } + + Sexp::List addrs; + for (auto&& addr : store().properties().personal_addresses) + addrs.add(Sexp::make_string(addr)); + + Sexp::List lst; + lst.add_prop(":pong", Sexp::make_string("mu")); + + Sexp::List proplst; + proplst.add_prop(":version", Sexp::make_string(VERSION)); + proplst.add_prop(":personal-addresses", Sexp::make_list(std::move(addrs))); + proplst.add_prop(":database-path", Sexp::make_string(store().properties().database_path)); + proplst.add_prop(":root-maildir", Sexp::make_string(store().properties().root_maildir)); + proplst.add_prop(":doccount", Sexp::make_number(storecount)); + proplst.add_prop(":queries", Sexp::make_list(std::move(qresults))); + + lst.add_prop(":props", Sexp::make_list(std::move(proplst))); + + output_sexp(std::move(lst)); +} + +void +Server::Private::quit_handler(const Parameters& params) +{ + keep_going_ = false; +} + +void +Server::Private::remove_handler(const Parameters& params) +{ + const auto docid{get_int_or(params, ":docid")}; + const auto path{path_from_docid(store(), docid)}; + + if (::unlink(path.c_str()) != 0 && errno != ENOENT) + throw Error(Error::Code::File, + "could not delete %s: %s", + path.c_str(), + g_strerror(errno)); + + if (!store().remove_message(path)) + g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid); + // act as if it worked. + + Sexp::List lst; + lst.add_prop(":remove", Sexp::make_number(docid)); + + output_sexp(std::move(lst)); +} + +void +Server::Private::sent_handler(const Parameters& params) +{ + const auto path{get_string_or(params, ":path")}; + const auto docid = store().add_message(path); + if (!docid) + throw Error{Error::Code::Store, "failed to add path"}; + + Sexp::List lst; + lst.add_prop(":sent", Sexp::make_symbol("t")); + lst.add_prop(":path", Sexp::make_string(path)); + lst.add_prop(":docid", Sexp::make_number(docid.value())); + + output_sexp(std::move(lst)); +} + +bool +Server::Private::maybe_mark_as_read(Store::Id docid, Flags oldflags, bool rename) +{ + const auto newflags{flags_from_delta_expr("+S-u-N", oldflags)}; + if (!newflags || oldflags == *newflags) + return false; // nothing to do. + + const auto msg = store().move_message(docid, {}, newflags, rename); + if (!msg) + throw msg.error(); + + /* send an update */ + Sexp::List update; + update.add_prop(":update", build_message_sexp(*msg, docid, {})); + output_sexp(Sexp::make_list(std::move(update))); + + g_debug("marked message %d as read => %s", docid, msg->path().c_str()); + + return true; +} + +bool +Server::Private::maybe_mark_msgid_as_read(const std::string& msgid, bool rename) try +{ + const auto docids = docids_for_msgid(store_, msgid); + if (!docids.empty()) + g_debug("marking %zu messages with message-id '%s' as read", + docids.size(), msgid.c_str()); + + for (auto&& docid: docids) + if (auto msg{store().find_message(docid)}; msg) + maybe_mark_as_read(docid, msg->flags(), rename); + + return true; + +} catch (...) { /* not fatal */ + g_warning("failed to mark <%s> as read", msgid.c_str()); + return false; +} + +void +Server::Private::view_handler(const Parameters& params) +{ + const auto mark_as_read{get_bool_or(params, ":mark-as-read")}; + /* for now, do _not_ rename, as it seems to confuse mbsync */ + const auto rename{false}; + //const auto rename{get_bool_or(params, ":rename")}; + + const auto docids{determine_docids(store(), params)}; + + if (docids.empty()) + throw Error{Error::Code::Store, "failed to find message for view"}; + + const auto docid{docids.at(0)}; + auto msg = store().find_message(docid) + .or_else([]{throw Error{Error::Code::Store, + "failed to find message for view"};}).value(); + + if (mark_as_read) { + // maybe mark the main message as read. + maybe_mark_as_read(docid, msg.flags(), rename); + /* maybe mark _all_ messsage with same message-id as read */ + maybe_mark_msgid_as_read(msg.message_id(), rename); + } + + Sexp::List seq; + seq.add_prop(":view", build_message_sexp(msg, docid, {})); + output_sexp(std::move(seq)); +} + +Server::Server(Store& store, Server::Output output) + : priv_{std::make_unique<Private>(store, output)} +{} + +Server::~Server() = default; + +bool +Server::invoke(const std::string& expr) noexcept +{ + return priv_->invoke(expr); +} diff --git a/lib/mu-server.hh b/lib/mu-server.hh new file mode 100644 index 0000000..95c7ffe --- /dev/null +++ b/lib/mu-server.hh @@ -0,0 +1,86 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_SERVER_HH__ +#define MU_SERVER_HH__ + +#include <memory> +#include <functional> + +#include <utils/mu-sexp.hh> +#include <utils/mu-utils.hh> +#include <mu-store.hh> + +namespace Mu { + +/** + * @brief Implements the mu server, as used by mu4e. + * + */ +class Server { +public: + enum struct OutputFlags { + None = 0, + SplitList = 1 << 0, + /**< insert newlines between list items */ + Flush = 1 << 1, + /**< flush output buffer after */ + }; + + /** + * Prototype for output function + * + * @param sexp an s-expression + * @param flags flags that influence the behavior + */ + using Output = std::function<void(Sexp&& sexp, OutputFlags flags)>; + + /** + * Construct a new server + * + * @param store a message store object + * @param output callable for the server responses. + */ + Server(Store& store, Output output); + + /** + * DTOR + */ + ~Server(); + + /** + * Invoke a call on the server. + * + * @param expr the s-expression to call + * + * @return true if we the server is still ready for more + * calls, false when it should quit. + */ + bool invoke(const std::string& expr) noexcept; + +private: + struct Private; + std::unique_ptr<Private> priv_; +}; +MU_ENABLE_BITOPS(Server::OutputFlags); + +} // namespace Mu + + +#endif /* MU_SERVER_HH__ */ diff --git a/lib/mu-store.cc b/lib/mu-store.cc new file mode 100644 index 0000000..f26a072 --- /dev/null +++ b/lib/mu-store.cc @@ -0,0 +1,716 @@ +/* +** Copyright (C) 2021-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include <chrono> +#include <memory> +#include <mutex> +#include <array> +#include <cstdlib> +#include <stdexcept> +#include <string> +#include <unordered_map> +#include <atomic> +#include <type_traits> +#include <iostream> +#include <cstring> + +#include <vector> +#include <xapian.h> + +#include "mu-maildir.hh" +#include "mu-store.hh" +#include "mu-query.hh" +#include "utils/mu-error.hh" + +#include "utils/mu-utils.hh" +#include "utils/mu-xapian-utils.hh" + +using namespace Mu; + +static_assert(std::is_same<Store::Id, Xapian::docid>::value, "wrong type for Store::Id"); + +// Properties +constexpr auto SchemaVersionKey = "schema-version"; +constexpr auto RootMaildirKey = "maildir"; // XXX: make this 'root-maildir' +constexpr auto ContactsKey = "contacts"; +constexpr auto PersonalAddressesKey = "personal-addresses"; +constexpr auto CreatedKey = "created"; +constexpr auto BatchSizeKey = "batch-size"; +constexpr auto DefaultBatchSize = 250'000U; + +constexpr auto MaxMessageSizeKey = "max-message-size"; +constexpr auto DefaultMaxMessageSize = 100'000'000U; +constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION; + +// Stats. +constexpr auto ChangedKey = "changed"; +constexpr auto IndexedKey = "indexed"; + + +static std::string +tstamp_to_string(::time_t t) +{ + char buf[17]; + ::snprintf(buf, sizeof(buf), "%" PRIx64, static_cast<int64_t>(t)); + return std::string(buf); +} + +static ::time_t +string_to_tstamp(const std::string& str) +{ + return static_cast<::time_t>(::strtoll(str.c_str(), {}, 16)); +} + +struct Store::Private { + enum struct XapianOpts { ReadOnly, Open, CreateOverwrite }; + + Private(const std::string& path, bool readonly) + : read_only_{readonly}, db_{make_xapian_db(path, + read_only_ ? XapianOpts::ReadOnly + : XapianOpts::Open)}, + properties_{make_properties(path)}, + contacts_cache_{db().get_metadata(ContactsKey), + properties_.personal_addresses} { + } + + Private(const std::string& path, + const std::string& root_maildir, + const StringVec& personal_addresses, + const Store::Config& conf) + : read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)}, + properties_{init_metadata(conf, path, root_maildir, personal_addresses)}, + contacts_cache_{"", properties_.personal_addresses} { + } + + ~Private() try { + + g_debug("closing store @ %s", properties_.database_path.c_str()); + if (!read_only_) { + transaction_maybe_commit(true /*force*/); + } + } catch (...) { + g_critical("caught exception in store dtor"); + } + + std::unique_ptr<Xapian::Database> make_xapian_db(const std::string db_path, XapianOpts opts) + try { + /* we do our own flushing, set Xapian's internal one as the + * backstop*/ + g_setenv("XAPIAN_FLUSH_THRESHOLD", "500000", 1); + + if (g_mkdir_with_parents(db_path.c_str(), 0700) != 0) + throw Mu::Error(Error::Code::Internal, + "failed to create database dir %s: %s", + db_path.c_str(), ::strerror(errno)); + + switch (opts) { + case XapianOpts::ReadOnly: + return std::make_unique<Xapian::Database>(db_path); + case XapianOpts::Open: + return std::make_unique<Xapian::WritableDatabase>(db_path, Xapian::DB_OPEN); + case XapianOpts::CreateOverwrite: + return std::make_unique<Xapian::WritableDatabase>( + db_path, + Xapian::DB_CREATE_OR_OVERWRITE); + default: + throw std::logic_error("invalid xapian options"); + } + + } catch (const Xapian::DatabaseLockError& xde) { + throw Mu::Error(Error::Code::StoreLock, + "%s", xde.get_msg().c_str()); + } catch (const Xapian::DatabaseError& xde) { + throw Mu::Error(Error::Code::Store, + "%s", xde.get_msg().c_str()); + } catch (const Mu::Error& me) { + throw; + } catch (...) { + throw Mu::Error(Error::Code::Internal, + "something went wrong when opening store @ %s", + db_path.c_str()); + } + + const Xapian::Database& db() const { return *db_.get(); } + + Xapian::WritableDatabase& writable_db() + { + if (read_only_) + throw Mu::Error(Error::Code::AccessDenied, "database is read-only"); + return dynamic_cast<Xapian::WritableDatabase&>(*db_.get()); + } + + // If not started yet, start a transaction. Otherwise, just update the transaction size. + void transaction_inc() noexcept + { + if (transaction_size_ == 0) { + g_debug("starting transaction"); + xapian_try([this] { writable_db().begin_transaction(); }); + } + ++transaction_size_; + } + + // Opportunistically commit a transaction if the transaction size + // filled up a batch, or with force. + void transaction_maybe_commit(bool force = false) noexcept { + if (force || transaction_size_ >= properties_.batch_size) { + if (contacts_cache_.dirty()) { + xapian_try([&] { + writable_db().set_metadata(ContactsKey, + contacts_cache_.serialize()); + }); + } + + if (indexer_) { // save last index time. + if (auto&& t{indexer_->completed()}; t != 0) + writable_db().set_metadata( + IndexedKey, tstamp_to_string(t)); + } + + if (transaction_size_ == 0) + return; // nothing more to do here. + + g_debug("committing transaction (n=%zu,%zu)", + transaction_size_, metadata_cache_.size()); + xapian_try([this] { + writable_db().commit_transaction(); + for (auto&& mdata : metadata_cache_) + writable_db().set_metadata(mdata.first, mdata.second); + transaction_size_ = 0; + }); + } + } + + time_t metadata_time_t(const std::string& key) const { + const auto ts = db().get_metadata(key); + return (time_t)atoll(db().get_metadata(key).c_str()); + } + + Store::Properties make_properties(const std::string& db_path) + { + Store::Properties props; + + props.database_path = db_path; + props.schema_version = db().get_metadata(SchemaVersionKey); + props.created = string_to_tstamp(db().get_metadata(CreatedKey)); + props.read_only = read_only_; + props.batch_size = ::atoll(db().get_metadata(BatchSizeKey).c_str()); + props.max_message_size = ::atoll(db().get_metadata(MaxMessageSizeKey).c_str()); + props.root_maildir = db().get_metadata(RootMaildirKey); + props.personal_addresses = Mu::split(db().get_metadata(PersonalAddressesKey), ","); + + return props; + } + + Store::Properties init_metadata(const Store::Config& conf, + const std::string& path, + const std::string& root_maildir, + const StringVec& personal_addresses) { + + writable_db().set_metadata(SchemaVersionKey, ExpectedSchemaVersion); + writable_db().set_metadata(CreatedKey, tstamp_to_string(::time({}))); + + const size_t batch_size = conf.batch_size ? conf.batch_size : DefaultBatchSize; + writable_db().set_metadata(BatchSizeKey, Mu::format("%zu", batch_size)); + const size_t max_msg_size = conf.max_message_size ? conf.max_message_size + : DefaultMaxMessageSize; + writable_db().set_metadata(MaxMessageSizeKey, Mu::format("%zu", max_msg_size)); + + writable_db().set_metadata(RootMaildirKey, canonicalize_filename(root_maildir, {})); + + std::string addrs; + for (const auto& addr : personal_addresses) { // _very_ minimal check. + if (addr.find(",") != std::string::npos) + throw Mu::Error(Error::Code::InvalidArgument, + "e-mail address '%s' contains comma", + addr.c_str()); + addrs += (addrs.empty() ? "" : ",") + addr; + } + writable_db().set_metadata(PersonalAddressesKey, addrs); + + return make_properties(path); + } + + Option<Message> find_message_unlocked(Store::Id docid) const; + Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid); + Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path); + + /* metadata to write as part of a transaction commit */ + std::unordered_map<std::string, std::string> metadata_cache_; + + const bool read_only_{}; + std::unique_ptr<Xapian::Database> db_; + + const Store::Properties properties_; + ContactsCache contacts_cache_; + std::unique_ptr<Indexer> indexer_; + + size_t transaction_size_{}; + std::mutex lock_; +}; + +Result<Store::Id> +Store::Private::update_message_unlocked(Message& msg, Store::Id docid) +{ + msg.update_cached_sexp(); + + return xapian_try_result([&]{ + writable_db().replace_document(docid, msg.document().xapian_document()); + g_debug("updated message @ %s; docid = %u", msg.path().c_str(), docid); + writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({}))); + return Ok(std::move(docid)); + }); +} + +Result<Store::Id> +Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace) +{ + msg.update_cached_sexp(); + + return xapian_try_result([&]{ + auto id = writable_db().replace_document( + field_from_id(Field::Id::Path).xapian_term(path_to_replace), + msg.document().xapian_document()); + + writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({}))); + return Ok(std::move(id)); + }); +} + +Option<Message> +Store::Private::find_message_unlocked(Store::Id docid) const +{ + return xapian_try([&]()->Option<Message> { + auto res = Message::make_from_document(db().get_document(docid)); + if (res) + return Some(std::move(res.value())); + else + return Nothing; + }, Nothing); +} + + +Store::Store(const std::string& path, Store::Options opts) + : priv_{std::make_unique<Private>(path, none_of(opts & Store::Options::Writable))} +{ + if (properties().schema_version == ExpectedSchemaVersion) + return; // all is good. + + // Now, it seems the schema versions do not match; shall we automatically + // update? + + if (none_of(opts & Store::Options::AutoUpgrade)) { + // not allowed to auto-upgrade, so we give up. + throw Mu::Error(Error::Code::SchemaMismatch, + "expected schema-version %s, but got %s; " + "cannot auto-upgrade; please use 'mu init'", + ExpectedSchemaVersion, + properties().schema_version.c_str()); + } + + // Okay, let's attempt an auto-upgrade. + g_info("attempt reinit database from schema %s --> %s", + properties().schema_version.c_str(), ExpectedSchemaVersion); + + Config conf; + conf.batch_size = properties().batch_size; + conf.max_message_size = properties().max_message_size; + + priv_.reset(); + priv_ = std::make_unique<Private>(path, + properties().root_maildir, + properties().personal_addresses, + conf); + // Now let's try again. + priv_.reset(); + priv_ = std::make_unique<Private>(path, none_of(opts & Store::Options::Writable)); + if (properties().schema_version != ExpectedSchemaVersion) + // Nope, we failed. + throw Mu::Error(Error::Code::SchemaMismatch, + "failed to auto-upgrade from %s to %s; " + "please use 'mu init'", + properties().schema_version.c_str(), + ExpectedSchemaVersion); +} + +Store::Store(const std::string& path, + const std::string& maildir, + const StringVec& personal_addresses, + const Store::Config& conf) + : priv_{std::make_unique<Private>(path, maildir, personal_addresses, conf)} +{ +} + +Store::Store(Store&& other) +{ + priv_ = std::move(other.priv_); + priv_->indexer_.reset(); +} + +Store::~Store() = default; + +const Store::Properties& +Store::properties() const +{ + return priv_->properties_; +} + +Store::Statistics +Store::statistics() const +{ + Statistics stats{}; + + stats.size = size(); + stats.last_change = string_to_tstamp(priv_->db().get_metadata(ChangedKey)); + stats.last_index = string_to_tstamp(priv_->db().get_metadata(IndexedKey)); + + return stats; +} + + + +const ContactsCache& +Store::contacts_cache() const +{ + return priv_->contacts_cache_; +} + +const Xapian::Database& +Store::database() const +{ + return priv_->db(); +} + +Indexer& +Store::indexer() +{ + std::lock_guard guard{priv_->lock_}; + + if (properties().read_only) + throw Error{Error::Code::Store, "no indexer for read-only store"}; + else if (!priv_->indexer_) + priv_->indexer_ = std::make_unique<Indexer>(*this); + + return *priv_->indexer_.get(); +} + +std::size_t +Store::size() const +{ + std::lock_guard guard{priv_->lock_}; + return priv_->db().get_doccount(); +} + +bool +Store::empty() const +{ + return size() == 0; +} + +Result<Store::Id> +Store::add_message(const std::string& path, bool use_transaction) +{ + if (auto msg{Message::make_from_path(path)}; !msg) + return Err(msg.error()); + else + return add_message(msg.value(), use_transaction); +} + +Result<Store::Id> +Store::add_message(Message& msg, bool use_transaction) +{ + std::lock_guard guard{priv_->lock_}; + + const auto mdir{maildir_from_path(msg.path(), + properties().root_maildir)}; + if (!mdir) + return Err(mdir.error()); + + if (auto&& res = msg.set_maildir(mdir.value()); !res) + return Err(res.error()); + /* add contacts from this message to cache; this cache + * also determines whether those contacts are _personal_, i.e. match + * our personal addresses. + * + * if a message has any personal contacts, mark it as personal; do + * this by updating the message flags. + */ + bool is_personal{}; + priv_->contacts_cache_.add(msg.all_contacts(), is_personal); + if (is_personal) + msg.set_flags(msg.flags() | Flags::Personal); + + if (use_transaction) + priv_->transaction_inc(); + + auto res = priv_->update_message_unlocked(msg, msg.path()); + if (!res) + return Err(res.error()); + + if (use_transaction) /* commit if batch is full */ + priv_->transaction_maybe_commit(); + + g_debug("added %smessage @ %s; docid = %u", + is_personal ? "personal " : "", msg.path().c_str(), *res); + + return res; +} + + +Result<Store::Id> +Store::update_message(Message& msg, Store::Id docid) +{ + std::lock_guard guard{priv_->lock_}; + + return priv_->update_message_unlocked(msg, docid); +} + +bool +Store::remove_message(const std::string& path) +{ + return xapian_try( + [&] { + std::lock_guard guard{priv_->lock_}; + const auto term{field_from_id(Field::Id::Path).xapian_term(path)}; + priv_->writable_db().delete_document(term); + priv_->writable_db().set_metadata( + ChangedKey, tstamp_to_string(::time({}))); + g_debug("deleted message @ %s from store", path.c_str()); + + return true; + }, + false); +} + +void +Store::remove_messages(const std::vector<Store::Id>& ids) +{ + std::lock_guard guard{priv_->lock_}; + + priv_->transaction_inc(); + + xapian_try([&] { + for (auto&& id : ids) { + priv_->writable_db().delete_document(id); + } + priv_->writable_db().set_metadata( + ChangedKey, tstamp_to_string(::time({}))); + }); + + priv_->transaction_maybe_commit(true /*force*/); +} + + +Option<Message> +Store::find_message(Store::Id docid) const +{ + std::lock_guard guard{priv_->lock_}; + + return priv_->find_message_unlocked(docid); +} + + +Result<Message> +Store::move_message(Store::Id id, + Option<const std::string&> target_mdir, + Option<Flags> new_flags, bool change_name) +{ + std::lock_guard guard{priv_->lock_}; + + auto msg = priv_->find_message_unlocked(id); + if (!msg) + return Err(Error::Code::Store, "cannot find message <%u>", id); + + const auto old_path = msg->path(); + const auto target_flags = new_flags.value_or(msg->flags()); + const auto target_maildir = target_mdir.value_or(msg->maildir()); + + /* 1. first determine the file system path of the target */ + const auto target_path = + maildir_determine_target(msg->path(), properties().root_maildir, + target_maildir,target_flags, change_name); + if (!target_path) + return Err(target_path.error()); + + /* 2. let's move it */ + if (const auto res = maildir_move_message(msg->path(), target_path.value()); !res) + return Err(res.error()); + + /* 3. file move worked, now update the message with the new info.*/ + if (auto&& res = msg->update_after_move( + target_path.value(), target_maildir, target_flags); !res) + return Err(res.error()); + + /* 4. update message worked; re-store it */ + if (auto&& res = priv_->update_message_unlocked(*msg, old_path); !res) + return Err(res.error()); + + /* 6. Profit! */ + return Ok(std::move(msg.value())); +} + +std::string +Store::metadata(const std::string& key) const +{ + // get metadata either from the (uncommitted) cache or from the store. + + std::lock_guard guard{priv_->lock_}; + + const auto it = priv_->metadata_cache_.find(key); + if (it != priv_->metadata_cache_.end()) + return it->second; + else + return xapian_try([&] { + return priv_->db().get_metadata(key); + }, ""); +} + +void +Store::set_metadata(const std::string& key, const std::string& val) +{ + // get metadata either from the (uncommitted) cache or from the store. + + std::lock_guard guard{priv_->lock_}; + + priv_->metadata_cache_.erase(key); + priv_->metadata_cache_.emplace(key, val); +} + + +time_t +Store::dirstamp(const std::string& path) const +{ + constexpr auto epoch = static_cast<time_t>(0); + const auto ts{metadata(path)}; + if (ts.empty()) + return epoch; + else + return static_cast<time_t>(strtoll(ts.c_str(), NULL, 16)); +} + +void +Store::set_dirstamp(const std::string& path, time_t tstamp) +{ + std::array<char, 2 * sizeof(tstamp) + 1> data{}; + const auto len = static_cast<size_t>( + g_snprintf(data.data(), data.size(), "%zx", tstamp)); + + set_metadata(path, std::string{data.data(), len}); +} + +bool +Store::contains_message(const std::string& path) const +{ + return xapian_try( + [&] { + std::lock_guard guard{priv_->lock_}; + const auto term{field_from_id(Field::Id::Path).xapian_term(path)}; + return priv_->db().term_exists(term); + }, + false); +} + +std::size_t +Store::for_each_message_path(Store::ForEachMessageFunc msg_func) const +{ + size_t n{}; + + xapian_try([&] { + std::lock_guard guard{priv_->lock_}; + Xapian::Enquire enq{priv_->db()}; + + enq.set_query(Xapian::Query::MatchAll); + enq.set_cutoff(0, 0); + + Xapian::MSet matches(enq.get_mset(0, priv_->db().get_doccount())); + constexpr auto path_no{field_from_id(Field::Id::Path).value_no()}; + for (auto&& it = matches.begin(); it != matches.end(); ++it, ++n) + if (!msg_func(*it, it.get_document().get_value(path_no))) + break; + }); + + return n; +} + +void +Store::commit() +{ + std::lock_guard guard{priv_->lock_}; + priv_->transaction_maybe_commit(true /*force*/); +} + + +std::size_t +Store::for_each_term(Field::Id field_id, Store::ForEachTermFunc func) const +{ + size_t n{}; + + xapian_try([&] { + /* + * Do _not_ take a lock; this is only called from + * the message parser which already has the lock + */ + std::vector<std::string> terms; + const auto prefix{field_from_id(field_id).xapian_term()}; + for (auto it = priv_->db().allterms_begin(prefix); + it != priv_->db().allterms_end(prefix); ++it) { + ++n; + if (!func(*it)) + break; + } + }); + + return n; +} + +std::mutex& +Store::lock() const +{ + return priv_->lock_; +} + +Result<QueryResults> +Store::run_query(const std::string& expr, + Field::Id sortfield_id, + QueryFlags flags, size_t maxnum) const +{ + return Query{*this}.run(expr, sortfield_id, flags, maxnum); +} + +size_t +Store::count_query(const std::string& expr) const +{ + return xapian_try([&] { + std::lock_guard guard{priv_->lock_}; + Query q{*this}; + return q.count(expr); }, 0); +} + +std::string +Store::parse_query(const std::string& expr, bool xapian) const +{ + return xapian_try([&] { + std::lock_guard guard{priv_->lock_}; + Query q{*this}; + + return q.parse(expr, xapian); + }, + std::string{}); +} diff --git a/lib/mu-store.hh b/lib/mu-store.hh new file mode 100644 index 0000000..49b172a --- /dev/null +++ b/lib/mu-store.hh @@ -0,0 +1,473 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_STORE_HH__ +#define __MU_STORE_HH__ + +#include <string> +#include <vector> +#include <mutex> +#include <ctime> + +#include "mu-contacts-cache.hh" +#include <xapian.h> + +#include <utils/mu-utils.hh> +#include <index/mu-indexer.hh> +#include <mu-query-results.hh> +#include <utils/mu-utils.hh> +#include <utils/mu-option.hh> + +#include <message/mu-message.hh> + +namespace Mu { + +class Store { +public: + using Id = Xapian::docid; /**< Id for a message in the store */ + static constexpr Id InvalidId = 0; /**< Invalid store id */ + + /** + * Configuration options. + * + * @param path + * @param readonly + */ + enum struct Options { + None = 0, /**< No specific options */ + Writable = 1 << 0, /**< Open in writable mode */ + AutoUpgrade = 1 << 1, /**< automatically re-initialize + * versions do not match */ + }; + + /** + * Make a store for an existing document database + * + * @param path path to the database + * @param options startup options + * + * A store or an error. + */ + static Result<Store> make(const std::string& path, + Options opts=Options::None) noexcept try { + return Ok(Store{path, opts}); + + } catch (const Mu::Error& me) { + return Err(me); + } + /* LCOV_EXCL_START */ + catch (...) { + return Err(Error::Code::Internal, "failed to create store"); + } + /* LCOV_EXCL_STOP */ + + + struct Config { + size_t max_message_size{}; + /**< maximum size (in bytes) for a message, or 0 for default */ + size_t batch_size{}; + /**< size of batches before committing, or 0 for default */ + }; + + /** + * Construct a store for a not-yet-existing document database + * + * @param path path to the database + * @param maildir maildir to use for this store + * @param personal_addresses addresses that should be recognized as + * 'personal' for identifying personal messages. + * @param config a configuration object + */ + static Result<Store> make_new(const std::string& path, + const std::string& maildir, + const StringVec& personal_addresses, + const Config& conf) noexcept try { + + return Ok(Store(path, maildir, personal_addresses, conf)); + + } catch (const Mu::Error& me) { + return Err(me); + } + /* LCOV_EXCL_START */ + catch (...) { + return Err(Error::Code::Internal, "failed to create new store"); + } + /* LCOV_EXCL_STOP */ + + /** + * Move CTOR + * + */ + Store(Store&&); + + /** + * DTOR + */ + ~Store(); + + /** + * Store properties + */ + struct Properties { + std::string database_path; /**< Full path to the Xapian database */ + std::string schema_version; /**< Database schema version */ + std::time_t created; /**< database creation time */ + + bool read_only; /**< Is the database opened read-only? */ + size_t batch_size; /**< Maximum database transaction batch size */ + bool in_memory; /**< Is this an in-memory database (for testing)?*/ + + std::string root_maildir; /**< Absolute path to the top-level maildir */ + + StringVec personal_addresses; /**< Personal e-mail addresses */ + size_t max_message_size; /**< Maximus allowed message size */ + }; + + /** + * Get properties about this store. + * + * @return the metadata + */ + const Properties& properties() const; + + + /** + * Store statistics. Unlike the properties, these can change + * during the lifetime of a store. + * + */ + struct Statistics { + size_t size; /**< number of messages in store */ + ::time_t last_change; /**< last time any update happened */ + ::time_t last_index; /**< last time an indexing op was performed */ + }; + + /** + * Get store statistics + * + * @return statistics + */ + Statistics statistics() const; + + + /** + * Get the ContactsCache object for this store + * + * @return the Contacts object + */ + const ContactsCache& contacts_cache() const; + + /** + * Get the underlying Xapian database for this store. + * + * @return the database + */ + const Xapian::Database& database() const; + + /** + * Get the Indexer associated with this store. It is an error to call + * this on a read-only store. + * + * @return the indexer. + */ + Indexer& indexer(); + + /** + * Run a query; see the `mu-query` man page for the syntax. + * + * Multi-threaded callers must acquire the lock and keep it + * at least as long as the return value. + * + * @param expr the search expression + * @param sortfieldid the sortfield-id. If the field is NONE, sort by DATE + * @param flags query flags + * @param maxnum maximum number of results to return. 0 for 'no limit' + * + * @return the query-results or an error. + */ + std::mutex& lock() const; + Result<QueryResults> run_query(const std::string& expr, + Field::Id sortfield_id = Field::Id::Date, + QueryFlags flags = QueryFlags::None, + size_t maxnum = 0) const; + + /** + * run a Xapian query merely to count the number of matches; for the + * syntax, please refer to the mu-query manpage + * + * @param expr the search expression; use "" to match all messages + * + * @return the number of matches + */ + size_t count_query(const std::string& expr = "") const; + + /** + * For debugging, get the internal string representation of the parsed + * query + * + * @param expr a xapian search expression + * @param xapian if true, show Xapian's internal representation, + * otherwise, mu's. + * + * @return the string representation of the query + */ + std::string parse_query(const std::string& expr, bool xapian) const; + + /** + * Add a message to the store. When planning to write many messages, + * it's much faster to do so in a transaction. If so, set + * @in_transaction to true. When done with adding messages, call + * commit(). + * + * @param path the message path. + * @param whether to bundle up to batch_size changes in a transaction + * + * @return the doc id of the added message or an error. + */ + Result<Id> add_message(const std::string& path, bool use_transaction = false); + + /** + * Add a message to the store. When planning to write many messages, + * it's much faster to do so in a transaction. If so, set + * @in_transaction to true. When done with adding messages, call + * commit(). + * + * @param msg a message + * @param whether to bundle up to batch_size changes in a transaction + * + * @return the doc id of the added message or an error. + */ + Result<Id> add_message(Message& msg, bool use_transaction = false); + + /** + * Update a message in the store. + * + * @param msg a message + * @param id the id for this message + * + * @return Ok() or an error. + */ + Result<Store::Id> update_message(Message& msg, Id id); + + /** + * Remove a message from the store. It will _not_ remove the message + * from the file system. + * + * @param path the message path. + * + * @return true if removing happened; false otherwise. + */ + bool remove_message(const std::string& path); + + /** + * Remove a number if messages from the store. It will _not_ remove the + * message from the file system. + * + * @param ids vector with store ids for the message + */ + void remove_messages(const std::vector<Id>& ids); + + /** + * Remove a message from the store. It will _not_ remove the message + * from the file system. + * + * @param id the store id for the message + */ + void remove_message(Id id) { remove_messages({id}); } + + /** + * Find message in the store. + * + * @param id doc id for the message to find + * + * @return a message (if found) or Nothing + */ + Option<Message> find_message(Id id) const; + + /** + * does a certain message exist in the store already? + * + * @param path the message path + * + * @return true if the message exists in the store, false otherwise + */ + bool contains_message(const std::string& path) const; + + /** + * Move a message both in the filesystem and in the store. + * After a successful move, the message is updated. + * + * @param id the id for some message + * @param target_mdir the target maildir (if any) + * @param new_flags new flags (if any) + * @param change_name whether to change the name + * + * @return Result, either the moved message or some error. + */ + Result<Message> move_message(Store::Id id, + Option<const std::string&> target_mdir = Nothing, + Option<Flags> new_flags = Nothing, + bool change_name = false); + + /** + * Prototype for the ForEachMessageFunc + * + * @param id :t store Id for the message + * @param path: the absolute path to the message + * + * @return true if for_each should continue; false to quit + */ + using ForEachMessageFunc = std::function<bool(Id, const std::string&)>; + + /** + * Call @param func for each document in the store. This takes a lock on + * the store, so the func should _not_ call any other Store:: methods. + * + * @param func a Callable invoked for each message. + * + * @return the number of times func was invoked + */ + size_t for_each_message_path(ForEachMessageFunc func) const; + + /** + * Prototype for the ForEachTermFunc + * + * @param term: + * + * @return true if for_each should continue; false to quit + */ + using ForEachTermFunc = std::function<bool(const std::string&)>; + + /** + * Call @param func for each term for the given field in the store. This + * takes a lock on the store, so the func should _not_ call any other + * Store:: methods. + * + * @param id the field id + * @param func a Callable invoked for each message. + * + * @return the number of times func was invoked + */ + size_t for_each_term(Field::Id id, ForEachTermFunc func) const; + + + /** + * Get the store metadata for @p key + * + * @param key the metadata key + * + * @return the metadata value or empty for none. + */ + std::string metadata(const std::string& key) const; + + /** + * Write metadata to the store. + * + * @param key key + * @param val value + */ + void set_metadata(const std::string& key, const std::string& val); + + /** + * Get the timestamp for some message, or 0 if not found + * + * @param path the path + * + * @return the timestamp, or 0 if not found + */ + time_t message_tstamp(const std::string& path) const; + + /** + * Get the timestamp for some directory + * + * @param path the path + * + * @return the timestamp, or 0 if not found + */ + time_t dirstamp(const std::string& path) const; + + /** + * Set the timestamp for some directory + * + * @param path a filesystem path + * @param tstamp the timestamp for that path + */ + void set_dirstamp(const std::string& path, time_t tstamp); + + /** + * Get the number of documents in the document database + * + * @return the number + */ + std::size_t size() const; + + /** + * Is the database empty? + * + * @return true or false + */ + bool empty() const; + + /** + * Commit the current batch of modifications to disk, opportunistically. + * If no transaction is underway, do nothing. + */ + void commit(); + + /** + * Get a reference to the private data. For internal use. + * + * @return private reference. + */ + struct Private; + std::unique_ptr<Private>& priv() { return priv_; } + const std::unique_ptr<Private>& priv() const { return priv_; } + +private: + /** + * Construct a store for an existing document database + * + * @param path path to the database + * @param options startup options + */ + Store(const std::string& path, Options opts=Options::None); + + /** + * Construct a store for a not-yet-existing document database + * + * @param path path to the database + * @param maildir maildir to use for this store + * @param personal_addresses addresses that should be recognized as + * 'personal' for identifying personal messages. + * @param config a configuration object + */ + Store(const std::string& path, + const std::string& maildir, + const StringVec& personal_addresses, + const Config& conf); + + + std::unique_ptr<Private> priv_; +}; + +MU_ENABLE_BITOPS(Store::Options); + +} // namespace Mu + +#endif /* __MU_STORE_HH__ */ diff --git a/lib/mu-tokenizer.cc b/lib/mu-tokenizer.cc new file mode 100644 index 0000000..14b318b --- /dev/null +++ b/lib/mu-tokenizer.cc @@ -0,0 +1,129 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include "mu-tokenizer.hh" +#include "utils/mu-utils.hh" + +#include <cctype> +#include <iostream> +#include <algorithm> + +using namespace Mu; + +static bool +is_separator(char c) +{ + if (isblank(c)) + return true; + + const auto seps = std::string("()"); + return seps.find(c) != std::string::npos; +} + +static Mu::Token +op_or_value(size_t pos, const std::string& val) +{ + auto s = val; + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + + if (s == "and") + return Token{pos, Token::Type::And, val}; + else if (s == "or") + return Token{pos, Token::Type::Or, val}; + else if (s == "xor") + return Token{pos, Token::Type::Xor, val}; + else if (s == "not") + return Token{pos, Token::Type::Not, val}; + else + return Token{pos, Token::Type::Data, val}; +} + +static void +unread_char(std::string& food, char kar, size_t& pos) +{ + food = kar + food; + --pos; +} + +static Mu::Token +eat_token(std::string& food, size_t& pos) +{ + bool quoted{}; + bool escaped{}; + std::string value{}; + + while (!food.empty()) { + const auto kar = food[0]; + food.erase(0, 1); + ++pos; + + if (kar == '\\') { + escaped = !escaped; + if (escaped) + continue; + } + + if (kar == '"') { + if (!escaped && quoted) + return Token{pos, Token::Type::Data, value}; + else { + quoted = true; + continue; + } + } + + if (!quoted && !escaped && is_separator(kar)) { + if (!value.empty() && kar != ':') { + unread_char(food, kar, pos); + return op_or_value(pos, value); + } + + if (quoted || isblank(kar)) + continue; + + switch (kar) { + case '(': return {pos, Token::Type::Open, "("}; + case ')': return {pos, Token::Type::Close, ")"}; + default: break; + } + } + + value += kar; + escaped = false; + } + + return {pos, Token::Type::Data, value}; +} + +Mu::Tokens +Mu::tokenize(const std::string& s) +{ + Tokens tokens{}; + + std::string food = utf8_clean(s); + size_t pos{0}; + + if (s.empty()) + return {}; + + while (!food.empty()) + tokens.emplace_back(eat_token(food, pos)); + + return tokens; +} diff --git a/lib/mu-tokenizer.hh b/lib/mu-tokenizer.hh new file mode 100644 index 0000000..7016e8b --- /dev/null +++ b/lib/mu-tokenizer.hh @@ -0,0 +1,139 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef __TOKENIZER_HH__ +#define __TOKENIZER_HH__ + +#include <string> +#include <vector> +#include <deque> +#include <ostream> +#include <stdexcept> + +// A simple tokenizer, which turns a string into a deque of tokens +// +// It recognizes '(', ')', '*' 'and', 'or', 'xor', 'not' +// +// Note that even if we recognizes those at the lexical level, they might be demoted to mere strings +// when we're creating the parse tree. +// +// Furthermore, we detect ranges ("a..b") and regexps (/../) at the parser level, since we need a +// bit more context to resolve ambiguities. + +namespace Mu { + +// A token +struct Token { + enum class Type { + Data, /**< e .g., banana or date:..456 */ + + // Brackets + Open, /**< ( */ + Close, /**< ) */ + + // Unops + Not, /**< logical not*/ + + // Binops + And, /**< logical and */ + Or, /**< logical not */ + Xor, /**< logical xor */ + + Empty, /**< nothing */ + }; + + size_t pos{}; /**< position in string */ + Type type{}; /**< token type */ + const std::string str{}; /**< data for this token */ + + /** + * operator== + * + * @param rhs right-hand side + * + * @return true if rhs is equal to this; false otherwise + */ + bool operator==(const Token& rhs) const + { + return pos == rhs.pos && type == rhs.type && str == rhs.str; + } +}; + +/** + * operator<< + * + * @param os an output stream + * @param t a token type + * + * @return the updated output stream + */ +inline std::ostream& +operator<<(std::ostream& os, Token::Type t) +{ + switch (t) { + case Token::Type::Data: os << "<data>"; break; + + case Token::Type::Open: os << "<open>"; break; + case Token::Type::Close: os << "<close>"; break; + + case Token::Type::Not: os << "<not>"; break; + case Token::Type::And: os << "<and>"; break; + case Token::Type::Or: os << "<or>"; break; + case Token::Type::Xor: os << "<xor>"; break; + case Token::Type::Empty: os << "<empty>"; break; + default: // can't happen, but pacify compiler + throw std::runtime_error("<<bug>>"); + } + + return os; +} + +/** + * operator<< + * + * @param os an output stream + * @param t a token + * + * @return the updated output stream + */ +inline std::ostream& +operator<<(std::ostream& os, const Token& t) +{ + os << t.pos << ": " << t.type; + + if (!t.str.empty()) + os << " [" << t.str << "]"; + + return os; +} + +/** + * Tokenize a string into a vector of tokens. The tokenization always succeeds, ie., ignoring errors + * such a missing end-". + * + * @param s a string + * + * @return a deque of tokens + */ +using Tokens = std::deque<Token>; +Tokens tokenize(const std::string& s); + +} // namespace Mu + +#endif /* __TOKENIZER_HH__ */ diff --git a/lib/mu-tree.hh b/lib/mu-tree.hh new file mode 100644 index 0000000..ce9093c --- /dev/null +++ b/lib/mu-tree.hh @@ -0,0 +1,159 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef TREE_HH__ +#define TREE_HH__ + +#include <vector> +#include <string> +#include <string_view> +#include <iostream> +#include <message/mu-fields.hh> + +#include <utils/mu-option.hh> +#include <utils/mu-error.hh> + +namespace Mu { + +struct FieldValue { + FieldValue(Field::Id idarg, const std::string valarg): + field_id{idarg}, val1{valarg} {} + FieldValue(Field::Id idarg, const std::string valarg1, const std::string valarg2): + field_id{idarg}, val1{valarg1}, val2{valarg2} {} + + const Field& field() const { return field_from_id(field_id); } + const std::string& value() const { return val1; } + const std::pair<std::string, std::string> range() const { return { val1, val2 }; } + + const Field::Id field_id; + const std::string val1; + const std::string val2; + +}; + + +/** + * operator<< + * + * @param os an output stream + * @param fval a field value. + * + * @return the updated output stream + */ +inline std::ostream& +operator<<(std::ostream& os, const FieldValue& fval) +{ + os << ' ' << quote(std::string{fval.field().name}); + + if (fval.field().is_range()) + os << ' ' << quote(fval.range().first) + << ' ' << quote(fval.range().second); + else + os << ' ' << quote(fval.value()); + + return os; +} + +// A node in the parse tree +struct Node { + enum class Type { + Empty, // only for empty trees + OpAnd, + OpOr, + OpXor, + OpAndNot, + OpNot, + Value, + Range, + Invalid + }; + + Node(Type _type, FieldValue&& fval) : type{_type}, field_val{std::move(fval)} {} + Node(Type _type) : type{_type} {} + Node(Node&& rhs) = default; + + Type type; + Option<FieldValue> field_val; + + static constexpr std::string_view type_name(Type t) { + switch (t) { + case Type::Empty: + return ""; + case Type::OpAnd: + return "and"; + case Type::OpOr: + return "or"; + case Type::OpXor: + return "xor"; + case Type::OpAndNot: + return "andnot"; + case Type::OpNot: + return "not"; + case Type::Value: + return "value"; + case Type::Range: + return "range"; + case Type::Invalid: + return "<invalid>"; + default: + return "<error>"; + } + } + + static constexpr bool is_binop(Type t) { + return t == Type::OpAnd || t == Type::OpAndNot || t == Type::OpOr || + t == Type::OpXor; + } +}; + +inline std::ostream& +operator<<(std::ostream& os, const Node& t) +{ + os << Node::type_name(t.type); + if (t.field_val) + os << t.field_val.value(); + + return os; +} + +struct Tree { + Tree(Node&& _node) : node(std::move(_node)) {} + Tree(Tree&& rhs) = default; + + void add_child(Tree&& child) { children.emplace_back(std::move(child)); } + bool empty() const { return node.type == Node::Type::Empty; } + + Node node; + std::vector<Tree> children; +}; + +inline std::ostream& +operator<<(std::ostream& os, const Tree& tree) +{ + os << '(' << tree.node; + for (const auto& subtree : tree.children) + os << subtree; + os << ')'; + + return os; +} + +} // namespace Mu + +#endif /* TREE_HH__ */ diff --git a/lib/mu-xapian.cc b/lib/mu-xapian.cc new file mode 100644 index 0000000..829667c --- /dev/null +++ b/lib/mu-xapian.cc @@ -0,0 +1,134 @@ +/* +** Copyright (C) 2017-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <config.h> + +#include <xapian.h> +#include "mu-xapian.hh" +#include <utils/mu-error.hh> + +using namespace Mu; + +static Xapian::Query +xapian_query_op(const Mu::Tree& tree) +{ + if (tree.node.type == Node::Type::OpNot) { // OpNot x ::= <all> AND NOT x + if (tree.children.size() != 1) + throw std::runtime_error("invalid # of children"); + return Xapian::Query(Xapian::Query::OP_AND_NOT, + Xapian::Query::MatchAll, + xapian_query(tree.children.front())); + } + + const auto op = std::invoke([](Node::Type ntype) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (ntype) { + case Node::Type::OpAnd: + return Xapian::Query::OP_AND; + case Node::Type::OpOr: + return Xapian::Query::OP_OR; + case Node::Type::OpXor: + return Xapian::Query::OP_XOR; + case Node::Type::OpAndNot: + return Xapian::Query::OP_AND_NOT; + case Node::Type::OpNot: + default: + throw Mu::Error(Error::Code::Internal, "invalid op"); // bug + } +#pragma GCC diagnostic pop + }, tree.node.type); + + std::vector<Xapian::Query> childvec; + for (const auto& subtree : tree.children) + childvec.emplace_back(xapian_query(subtree)); + + return Xapian::Query(op, childvec.begin(), childvec.end()); +} + +static Xapian::Query +make_query(const FieldValue& fval, bool maybe_wildcard) +{ + const auto vlen{fval.value().length()}; + if (!maybe_wildcard || vlen <= 1 || fval.value()[vlen - 1] != '*') + return Xapian::Query(fval.field().xapian_term(fval.value())); + else + return Xapian::Query(Xapian::Query::OP_WILDCARD, + fval.field().xapian_term(fval.value().substr(0, vlen - 1))); +} + +static Xapian::Query +xapian_query_value(const Mu::Tree& tree) +{ + // indexable field implies it can be use with a phrase search. + const auto& field_val{tree.node.field_val.value()}; + if (!field_val.field().is_indexable_term()) { // + /* not an indexable field; no extra magic needed*/ + return make_query(field_val, true /*maybe-wildcard*/); + } + + const auto parts{split(field_val.value(), " ")}; + if (parts.empty()) + return Xapian::Query::MatchNothing; // shouldn't happen + else if (parts.size() == 1) + return make_query(field_val, true /*maybe-wildcard*/); + + std::vector<Xapian::Query> phvec; + for (const auto& p : parts) { + FieldValue fv{field_val.field_id, p}; + phvec.emplace_back(make_query(fv, false /*no wildcards*/)); + } + + return Xapian::Query(Xapian::Query::OP_PHRASE, phvec.begin(), phvec.end()); +} + +static Xapian::Query +xapian_query_range(const Mu::Tree& tree) +{ + const auto& field_val{tree.node.field_val.value()}; + + return Xapian::Query(Xapian::Query::OP_VALUE_RANGE, + field_val.field().value_no(), + field_val.range().first, + field_val.range().second); +} + +Xapian::Query +Mu::xapian_query(const Mu::Tree& tree) +{ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (tree.node.type) { + case Node::Type::Empty: + return Xapian::Query(); + case Node::Type::OpNot: + case Node::Type::OpAnd: + case Node::Type::OpOr: + case Node::Type::OpXor: + case Node::Type::OpAndNot: + return xapian_query_op(tree); + case Node::Type::Value: + return xapian_query_value(tree); + case Node::Type::Range: + return xapian_query_range(tree); + default: + throw Mu::Error(Error::Code::Internal, "invalid query"); // bug + } +#pragma GCC diagnostic pop +} diff --git a/lib/mu-xapian.hh b/lib/mu-xapian.hh new file mode 100644 index 0000000..54ee006 --- /dev/null +++ b/lib/mu-xapian.hh @@ -0,0 +1,39 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef MU_XAPIAN_HH__ +#define MU_XAPIAN_HH__ + +#include <xapian.h> +#include <mu-parser.hh> + +namespace Mu { + +/** + * Transform a parse-tree into a Xapian query object + * + * @param tree a parse tree + * + * @return a Xapian query object + */ +Xapian::Query xapian_query(const Mu::Tree& tree); + +} // namespace Mu + +#endif /* MU_XAPIAN_H__ */ diff --git a/lib/tests/bench-indexer.cc b/lib/tests/bench-indexer.cc new file mode 100644 index 0000000..b83bd90 --- /dev/null +++ b/lib/tests/bench-indexer.cc @@ -0,0 +1,550 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include <glib.h> +#include <string> +#include <thread> +#include <vector> +#include <iostream> +#include <regex> +#include <fstream> + +#include <utils/mu-utils.hh> +#include <mu-store.hh> +#include "mu-maildir.hh" + +#include "utils/mu-test-utils.hh" + +using namespace Mu; + +constexpr auto test_msg = +R"(Return-Path: <htcondor-users-bounces@cs.wisc.edu> +Received: from pop3.web.de [212.227.17.177] + by localhost with POP3 (fetchmail-6.4.6) + for <arne@localhost> (single-drop); Fri, 26 Jun 2020 12:56:08 +0200 (CEST) +Received: from jeeves.cs.wisc.edu ([128.105.6.16]) by mx-ha.web.de (mxweb112 + [212.227.17.8]) with ESMTPS (Nemesis) id 1MdMYE-1jFXaM2gnA-00ZKvt for + <@ID@@web.de>; Fri, 26 Jun 2020 01:28:11 +0200 +Received: from jeeves.cs.wisc.edu (localhost [127.0.0.1]) + by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLgek013419; + Thu, 25 Jun 2020 18:22:23 -0500 +Received: from shale.cs.wisc.edu (shale.cs.wisc.edu [128.105.6.25]) + by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaf0013414 + (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=OK) + for <htcondor-users@jeeves.cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500 +Received: from smtp7.wiscmail.wisc.edu (wmmta4.doit.wisc.edu [144.92.197.245]) + by shale.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaMK013694 + (version=TLSv1/SSLv3 cipher=DHE-RSA-AES128-GCM-SHA256 bits=128 + verify=NO) + for <htcondor-users@cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500 +Received: from USG02-CY1-obe.outbound.protection.office365.us + ([23.103.209.108]) by smtp7.wiscmail.wisc.edu + (Oracle Communications Messaging Server 8.0.2.4.20190812 64bit (built + Aug 12 + 2019)) with ESMTPS id <0QCI042LC8VUXFC0@smtp7.wiscmail.wisc.edu> for + htcondor-users@cs.wisc.edu (ORCPT htcondor-users@cs.wisc.edu); Thu, + 25 Jun 2020 18:21:31 -0500 (CDT) +X-Spam-Report: IsSpam=no, Probability=11%, Hits= RETURN_RECEIPT 0.5, + FROM_US_TLD 0.1, HTML_00_01 0.05, HTML_00_10 0.05, SUPERLONG_LINE 0.05, + BODYTEXTP_SIZE_3000_LESS 0, BODY_SIZE_10000_PLUS 0, DKIM_SIGNATURE 0, + KNOWN_MTA_TFX 0, NO_URI_HTTPS 0, SPF_PASS 0, SXL_IP_TFX_WM 0, + WEBMAIL_SOURCE 0, WEBMAIL_XOIP 0, WEBMAIL_X_IP_HDR 0, __ANY_URI 0, + __ARCAUTH_DKIM_PASSED 0, __ARCAUTH_DMARC_PASSED 0, __ARCAUTH_PASSED 0, + __ATTACHMENT_SIZE_0_10K 0, __ATTACHMENT_SIZE_10_25K 0, + __BODY_NO_MAILTO 0, + __CT 0, __CTYPE_HAS_BOUNDARY 0, __CTYPE_MULTIPART 0, __HAS_ATTACHMENT 0, + __HAS_ATTACHMENT1 0, __HAS_ATTACHMENT2 0, __HAS_FROM 0, __HAS_MSGID 0, + __HAS_XOIP 0, __HIGHBITS 0, __MIME_TEXT_P 0, __MIME_TEXT_P1 0, + __MIME_TEXT_P2 0, __MIME_VERSION 0, __MULTIPLE_RCPTS_TO_X2 0, + __NO_HTML_TAG_RAW 0, __RETURN_RECEIPT_TO 0, __SANE_MSGID 0, + __TO_MALFORMED_2 0, __TO_NAME 0, __TO_NAME_DIFF_FROM_ACC 0, + __TO_NO_NAME 0, + __TO_REAL_NAMES 0, __URI_IN_BODY 0, __URI_MAILTO 0, __URI_NOT_IMG 0, + __URI_NO_PATH 0, __URI_NS , __URI_WITHOUT_PATH 0 +X-Wisc-Doma: @ID@X@numerica.us,numerica.us +X-Wisc-Env-From-B64: d2VzbGV5LnRheWxvckBudW1lcmljYS51cw== +X-Spam-PmxInfo: Server=avs-13, Version=6.4.7.2805085, + Antispam-Engine: 2.7.2.2107409, Antispam-Data: 2020.6.25.231519, + AntiVirus-Engine: 5.74.0, AntiVirus-Data: 2020.6.25.5740002, + SenderIP=[23.103.209.108] +X-Wisc-DKIM-Verify: @ID@XXXXXXX@numerica.us,numericaus.onmicrosoft.com!pass +X-Spam-Score: * +ARC-Seal: i=1; a=rsa-sha256; s=arcselector5401; d=microsoft.com; cv=none; + b=KyXoddJsnsHsBwhdlO5rcljgMRaylJAUAxWTjG4jQL1C8XJAMgeERtH2sRffdjibYUFfSuDUNJmrTrvrbjKGUt2I8J2M2MgUB/upMoroVPNBrP1Fy9wMeZJQuSS4r4KjZZktsl2i8eq667pzOZO6+wX2IA5M7YtxDqglcWOE6btWzbABVjx+9eCXMt0eMd1+UI6ABK8Frd33EFQLKT0h/cxidWR9l+0gCMAcRxsLrQ82+ckU606AIV/DA1E4Tq7ADe/+CRv4QszDN93pWL/1N2/OOh9vFTs9g9ZG6uXjN+Km/IAdylPbfHgKW60ev3/Bvv6N3pA7DjpuiKj6BnW7mQ== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; + s=arcselector5401; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; + bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=; + b=Pq07a3u26s2UdpucJuVQ0h68272wx46Wp61x/30TelPPFLCRxVjmlH1U3IBmIsZ1jOEtGXFJRv65L3HmwGxRUdLlMOdPRB64BBfHQ9NGWUBykKQmOrJNGJs635nEdpugpzngzIdcg1PS5vHxPJAnOeqoo71OVPI3JqPrPEn2TJJgb9J6PApexkqIbVl35prGPsyS/t2IlYw3/ihWzORG6wvqJeqedgpJTBXeGaDoMa+MQ1BeUsdvybh8+hau4ASpM5lwyeXlGmJ5mUTZi39jp+dFdDrmCj/VM4ezeuXeH9+HFtDjKLZJaTDWUID0IBcr91BaoQE/4r6y+lpkah6LLQ== +ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass + smtp.mailfrom=numerica.us; + dmarc=pass action=none header.from=numerica.us; + dkim=pass header.d=numerica.us; arc=none +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=numericaus.onmicrosoft.com; s=selector1-numericaus-onmicrosoft-com; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; + bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=; + b=cFn0eL5k2IKry9U8qa8mbVaxRiyicUAWzRc3NUtj+VEbgShfrz8SO6FPX20WTQQJg/Fu/3isqsSEUt+9NSEEbgd5eQ1EVz5E/JVeNjPe9GXR0JEF/g3f6yM7CO+kKTvXSRvQjce683U0j7Aj1pSDEktoVNP4xvOS2Gx9VjdWTmc= +Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::10) + by DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::14) + with Microsoft SMTP Server + (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) + id 15.20.3109.25; Thu, 25 Jun 2020 23:21:07 +0000 +Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM + ([fe80::f548:f084:9867:9375]) by DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM + ([fe80::f548:f084:9867:9375%11]) with mapi id 15.20.3131.024; Thu, + 25 Jun 2020 23:21:07 +0000 +From: Raul Endymion <XXXXXXXXXXXXXXXXXXXX@numerica.us> +To: "'htcondor-users@cs.wisc.edu'" <htcondor-users@cs.wisc.edu> +Thread-topic: OPINIONS WANTED: Are there any blatent downsides I am missing to + the following Condor configuration +Thread-index: AdZLRbEvYoEDBZChS62aOHgPzKD8kw== +Date: Thu, 25 Jun 2020 23:21:06 +0000 +Message-id: <DM3P110MB04746CDBA55B3E597EFD1877FA920@DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM> +Accept-Language: en-US +Content-language: en-US +X-MS-Has-Attach: yes +X-MS-TNEF-Correlator: +X-Originating-IP: [50.233.29.54] +x-ms-publictraffictype: Email +x-ms-office365-filtering-correlation-id: f4edecdf-5582-4b2d-226a-08d8195e7007 +x-ms-traffictypediagnostic: DM3P110MB0490: +x-microsoft-antispam-prvs: <DM3P110MB04907C313C4243B4FE42A20CFA920@DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM> +x-ms-oob-tlc-oobclassifiers: OLM:10000; +x-forefront-prvs: 0445A82F82 +x-ms-exchange-senderadcheck: 1 +x-microsoft-antispam: BCL:0; +X-Forefront-Antispam-Report: CIP:255.255.255.255; CTRY:; LANG:en; SCL:1; SRV:; + IPV:NLI; SFV:NSPM; H:DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM; + PTR:; CAT:NONE; SFTY:; + SFS:(346002)(366004)(6916009)(83380400001)(71200400001)(8936002)(55016002)(5660300002)(8676002)(9686003)(66616009)(64756008)(33656002)(52536014)(66446008)(66476007)(66556008)(66946007)(99936003)(76116006)(186003)(86362001)(26005)(7696005)(6506007)(44832011)(508600001)(2906002)(80162005)(80862006)(491001)(554374003); + DIR:OUT; SFP:1102; +x-ms-exchange-transport-forked: True +MIME-version: 1.0 +X-OriginatorOrg: numerica.us +X-MS-Exchange-CrossTenant-Network-Message-Id: f4edecdf-5582-4b2d-226a-08d8195e7007 +X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Jun 2020 23:21:06.8341 (UTC) +X-MS-Exchange-CrossTenant-fromentityheader: Hosted +X-MS-Exchange-CrossTenant-id: fae7a2ae-df1d-444e-91be-babb0900b9c2 +X-MS-Exchange-CrossTenant-mailboxtype: HOSTED +X-MS-Exchange-Transport-CrossTenantHeadersStamped: DM3P110MB0490 +Subject: [HTCondor-users] OPINIONS WANTED: Are there any blatent downsides I + am missing to the following Condor configuration +X-BeenThere: htcondor-users@cs.wisc.edu +X-Mailman-Version: 2.1.19 +Precedence: list +List-Id: HTCondor-Users Mail List <htcondor-users.cs.wisc.edu> +List-Unsubscribe: <https://lists.cs.wisc.edu/mailman/options/htcondor-users>, + <mailto:htcondor-users-request@cs.wisc.edu?subject=unsubscribe> +List-Archive: <https://www-auth.cs.wisc.edu/lists/htcondor-users/> +List-Post: <mailto:htcondor-users@cs.wisc.edu> +List-Help: <mailto:htcondor-users-request@cs.wisc.edu?subject=help> +List-Subscribe: <https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users>, + <mailto:htcondor-users-request@cs.wisc.edu?subject=subscribe> +Reply-To: HTCondor-Users Mail List <htcondor-users@cs.wisc.edu> +Content-Type: multipart/mixed; boundary="===============0678627779074767862==" +Errors-To: htcondor-users-bounces@cs.wisc.edu +Sender: "HTCondor-users" <htcondor-users-bounces@cs.wisc.edu> +Envelope-To: <@ID@XXXXX@web.de> +X-UI-Filterresults: unknown:2;V03:K0:cdojl5YHfkg=:jhTbQXp38SL2za/LB4M7aUwpyw + 5rDHoN1+/ScH/O9/G1fKWbGryQ203thF+1ZrHUOOwq8MVOc5SsoqzSTsaNbEAdthFcDDz3Oui + SHxX1hdpV3UOjZEHzWlpjEjRe7t74g2RI/ESELmkPuLg/LZC7SjAsg70cTJBIfDPYxJkJAcUl + 9W6OEBsmtTDO0va/EQRYjfkpoF9tjfmfMNw9KSKHuDdqZu2Xfak8mQKnWsoxWeUkD31r60iPC + yikbj7KP5AlHaWMzyTTdlvtjYRLfSuUSe1uqjI5NWCnZDDjz7zODoaWPp7p2U/MQenXEjN6+M + WnZL6ZC8AGtze/hYgOCXcLf4ydQ7m9YueJiY5nDn7g+cwnhxypVNFTL5NjSpKKXbkzbyu9Tdl + ez+92g/9pGW17iOo5NrFtfctLlmCEH0RxjouKI7FBmv3bIvFC4FvfghiNf7OZmRg2/nT5i+1o + AICYNAx2y5CezKsKM2f1tm60dkydQIR8pK45dDKZPz3i7NeJm9dknZ2OYFTnucUvdPaT8nR43 + cK3kk2QUE48Ngo/0NwepSGrV9TkOt+hY3PUYkXWp/mwP2QPSjy4cALyvLyKwG24qZ9CiiRLMV + KqPFlCRnoDG5MHJ4d0krFlqmg8rNsWzV3oWfMNKFZmD24lVUmWGb+ExxbCFc0xzIt12o/EqBw + nVkXLu+E0apM+cmG6ubYfOymRoUpiKsZI9ivc+mAaEE+v2RBzcAURzlhzQHIn81onvzbQwCge + tMtBkSEfqyoa1HjalX4B9WQ90M42K+7xW039ydakQ7JOeYVpkPXYoBF7mbrXRckhMXjQatLQ8 + MWA8+U031Xfa1ueOIfCCkzJ49wyx1LoLPyqdjCvnzaRd72yNEMJ5zM/itMIPE9reIHBtpom0i + RhIYdJFDrL+SKqE68lcJakCcF3R+VLApwLKOr0HChGQjdEk7c/rm5E0dF5f3oYlHf591QoXIJ + h16yfcJYe6fMo1YYunkvbEDFPpzttIq7aIk0FzxrOdRvj3yQajDbwOpYI/5T/DaabPn3M8lK7 + 8pn7LrbmyCaLHhkYMS4h3SDkYWsifza6vkldizrK7IPf6KhS7AhTkbnEonWS6454GLUg1nYGX + W5Qp/G9LzvjtEGQMcwnCN5jb5zq7o3f+9FrROKjpwFxL+mL85CEXY/KMOVpf6hDJJfSyu6/X6 + FpbwlJVLFdGeA0/+xcKcmutpkJACgK2kHqvZ8MZxt+5jBJWVlIDLZKa8/IoGWC+ikLX2/hPNB + 4TU89QYG5ygPmwwDXruFG7N7jVURZceHqWNKtqegS6YQ5nirsPJWJR7jzgr+HbntUaQETXNpn + QrxpsVHXfqRu2GlP5h28RaIpvBVUcwqrs+eLJELStvBzyAmaVPVoKFjEWFfwrmE89W6Bmz2W3 + kHExOq3hI3gDsGXKjTjT/kjHkaHmtnVUXr4vqovf8Ht4Vwmtf0S4xsgpYjnYjUIzG9eiwIFAZ + hL2gvjwW51qtMvybf01C50xTiS9GSfO0SR7meBPA67skcA+wFo11wmwXsUk1irpKnC+Y9hVZX + 2vPkfZ1T2VXNo997cQC59lBpi/TU5gnuM7H/Vcl7tF3Lqtmqut7s6HkPWCegDZ3O2W7shH7aZ + 1bOXbO+W/SNC+WcMnj+fhuP+dHcrt0Vw4RD9knJOOzdZTH3OCli/vpjqgTbCKEaWMhCIeM2g0 + RiLFxTeTEEBCa49bwa8n2r4T/vA3duZd8F/DNKvWTfhRr1Mxtz3n15EOar13fFijtnieEiv4/ + vO/5uRF+H86Fcoua7B8AswThbiG1vou6M48g0Zo6iGEcrueKEaHMI4XM7wQF77KazMdn5f1BP + +KyQX83aHJN/qGniXgF8yu+h0M7Nf0YrTteYQd2C/HZrIA8IaLqqvLoGRl7dRBnbZiP7jRdQm + 1YEYtjX4XBoShrXPfIxPnJBUBnnOaePYxOJkS2FaBv19jPkMnyc9xuJYD7JOTFnXKzAnoaBqT + OR+dGrLLGZ1MM/0gqclKTv7Hcce+6CJyTWkx5mq42w49HFI/kdHBRxU8xIRv4B9l0ePf9EbWr + cDcrssee//6KXiRmF4fm7jq828/uhj8MIJet9sIU5ncKwHEse3I4YmVT5+dB+ZGZh0gbJPFj6 + xcICpshhYct+euMCdNfy3lkxiRr76RwfBzLAOP5+1U3GAx/hcsL2AgyBHMwWo+Kkeq8pPy4YI + pQMxJyylI6JMa/DbBggnDk+xNZpRKo/XA4lAJY57DCOPL2ZcL8kU2aCd5LjtYHK0ZWSFtOjxs + oIEr/f2vvg+zibxzaANBzylZn3yPe9pI/IBefu9fL4MVaYY3aboxuncX4fyi0VH0WbFkSYXRi + a7LIu3LI2LTU13C/LE7j9hmxP6TApyiXi14f0GSa2sbF6HWp2v2rhYM7h67AAn3SQgvcJLpgb + Hz5ABb/OAk6ABVEl+a483zexJ6iT2P0gYc08zmewy8Jf8AD9r846k9pGZuhBaOHREx3bA16Bj + uWYh3QzSI6MQoJM3XbBGLVkX36Lfj54T9kk97lLaxfbGPuNoyOV9iTBKxts3m2KD+52iH3EEi + glbH6HNIUHyCHdEXsXyGVFwfM9V7OQcVO/g266KIQ74wU16x/Zdsq4p/1PcRXHRnoMxP/pUrj + EOLWzFU71qzC/OSkYWRil9HXUyucTFGQ0N08jZNXctI9lElWtgq3iI+Cz2F20rz+LJGhSHSkZ + 0G5JgXrtspeJN5yoH6TOE0hblr5sZcAM0wiSP7x/hPBeYHswzTA5/laWMn++9aTPVgpPaJ9/x + wyLm55OZr4Jl+StWd3MqLCgiRB3cNGrDX7f8Eqnj4wfCHiGIUHewD4qrfXraZQhIk17W+9JyD + osmUiVD9ZRdNCY2eNnu8ZkJ4uzKl44lwLL43sInKBjdAHlnoxrR2FOrYXbnU31ujwxdeUr6Hs + xPFy0Git0CpWCWYmaz37KA8GW7PE4ffWzcfCmz6AKBrbHcCreeUnyqnSEDy9ubnz7mcLRnu3W + RAWi6diI8gcS9g0+r4z5PtZX9rveXRekHJ4k08VuYVmdiz3gjXmHPlm9IKPEAbygP2EYgjwGE + RbReLc8xHJlfLbwdXyGw0HU= + +--===============0678627779074767862== +Content-language: en-US +Content-type: multipart/signed; protocol="application/x-pkcs7-signature"; + micalg=2.16.840.1.101.3.4.2.3; + boundary="----=_NextPart_000_0018_01D64B14.F58791A0" + +------=_NextPart_000_0018_01D64B14.F58791A0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +Hey! + +I am architecting our final HTCondor configuration over here and I have = +an idea I am unsure about and I would like to ask some experienced users = +for their opinion. + +Background, we have a small, relatively homogenous cluster (with no = +special universes) and less than 10 users. Since each user has their own = +workstation separate from our cluster I thought the following = +configuration would suit our needs, but I want to make sure there isn't = +a huge disadvantage I am missing: + +1. Set the Central Manager to be highly available to the point of = +tolerating N cluster machine failures +2. Put a Submit on each of the users' workstations (I am a little = +worried about the resource usage of the condor_shadow and condor_schedd, = +my users are already running into RAM consumption issues over time as it = +is) +3. Place an Execute on each of the cluster machines, which would lead to = +the central manager being on a machine that is also executing jobs + +Fortunately both my users' and cluster machines all have access to the = +same network storage, and we have centralized authentication so we can = +just use our users' credentials to authenticate everywhere.=20 + +Before I set this in dry mud, does anyone have any retrospective = +recommendations I could benefit hearing from, since I am still pretty = +new to the project? + +Thank you! +-Raul + +Raul Endymion =E2=80=93 Cluster Manager +Numerica Corporation (www.numerica.us) +5042 Technology Parkway #100 +Fort Collins, Colorado 80528 +=E2=98=8E=EF=B8=8F (970) 207 2233 +=F0=9F=93=A7 @ID@XXXXXXXXXXXXXXX@numerica.us + + + +------=_NextPart_000_0018_01D64B14.F58791A0 +Content-Type: application/pkcs7-signature; + name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="smime.p7s" + +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 +V/zEGk/PEc3pZxNAo7m0I/SXdNulUEkjxai5Wq53i0EhWVLpUU8XY3joXax46yCMqh0PUn90QmMD +BybLyFDX6av8tVS5cQs0HbTZdIuj7A/dsKzKKIrSHd3SQ9MLNPRkSRdhagmf5LCF1Y4xEEiuAA/H +XdYAxGIcl8n6b2CcLlZzq4W13Ipv8FIZoDsG1u0b9NGfeSOOHidi5kdD6r8lM5PaSPmZsl5PdKK6 ++E1Y6rBCvITu0MBo5Tjuwt5cok3Ve0BK5Fg89aIL2/rMicm20qG6nbqxLhHeR0mhPO98KIIzDoeL +rLpAlWS7GoPvJqbRzxsCAwEAAaOCAlYwggJSMBAGCSsGAQQBgjcVAQQDAgEBMCMGCSsGAQQBgjcV +AgQWBBSv5TU1Bjnw5n3u1iO2y+BHQXk7MTAdBgNVHQ4EFgQUoeMyqBhiyBcgwJN8zbr7pRbgs+sw +GQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHwYDVR0jBBgwFoAUF+CLMX/eZk96ElRSeiEHqnsujqEwgdMGA1UdHwSByzCByDCBxaCBwqCB +v4aBvGxkYXA6Ly8vQ049YWQtR0lMR0FMQUQtQ0EsQ049R2lsZ2FsYWQsQ049Q0RQLENOPVB1Ymxp +YyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQsREM9 +bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNz +PWNSTERpc3RyaWJ1dGlvblBvaW50MIHGBggrBgEFBQcBAQSBuTCBtjCBswYIKwYBBQUHMAKGgaZs +ZGFwOi8vL0NOPWFkLUdJTEdBTEFELUNBLENOPUFJQSxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNl +cyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWFkLERDPW51bWVyaWNhLERDPXVzP2NB +Q2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MA0GCSqG +SIb3DQEBCwUAA4ICAQBmRoSlPe++k7tsAJOvq0+0dNI6yk6gOBmY4g5jL9NTEjSxPWkeYegIwLr2 +UqpiIIZmAh9e9v3z0T2egVyRqNezLPXLkg/2gUfV6D0kRyKtG5mL0yAn/0hkkVyf6jWJpCKmH77x +0w3UpnfKs79jv5YpQDhC2eRFivN50HhIkigLWScPq4zd81ghmN8VFTHVQmsGua/mm1Oj5/pBFuQF +B4ljon1N//wX5ZJZaUlJR9eR9tM9m+Gyds2flr5+mZT6Zgm26fKiC5zs91aGnzqGx6s30jfXELP2 +FjFrrR46ooV7ehhnyBlCACxIWqXe5sSZsSh9oEYZ7Ux5Vq0thkfArBWsF7HA+LovKCUyHLcXbVBB +6/VAwZ3GLYi/bqbVIEFlVRu4nv/JyKWwoGbAhGyzZNWoeHszFrEIQbQMoMsEumVkMZreE6AxP+zb +6JPPOjlhpymtMo54z1MDYJPyo4HmcpL4xUjHZgqgOxMrbHC4oIVLvKZ/scbVBhPnd0tHHSZqj3ZS +gfTvG/ut/tLNTXXe48PkLBw4KguhbLm61Elu3wJALT0UL+ENgUWwb7csUGQBqOyPAHXGYnf/ACOc +UBqQckcrK8Jq3u8rnCloW3uDw86hw7MFM+YjmhVRdYRxpJmhKVPT6Amufp2WsSVId8q3CSqTH33L +fcxbV1n7hLWHA67MhTCCBq0wggWVoAMCAQICEycAAAsJMaw2RjtHZFUAAQAACwkwDQYJKoZIhvcN +AQELBQAwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYKCZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQ +BgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VMRUJSSUFOLUNBMB4XDTIwMDUxMjE1MDk0 +MloXDTIxMDcyMjE0MjgwMVowgcExEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkW +CG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxETAPBgNVBAsTCE51bWVyaWNhMQ4wDAYDVQQL +EwVVc2VyczEYMBYGA1UECxMPUHJlc2VudCBJbnRlcm5zMRYwFAYDVQQDEw1XZXNsZXkgVGF5bG9y +MSgwJgYJKoZIhvcNAQkBFhl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5clDLapXkiLVXhAFP9GJv+JJkt+cacyvWaX9xEvqMQXOXb7MqO5E +DJE8XPMfxaX84WhuMMePOc9SNUKpDtTa2SHz+AOom+JH38ce2gfrdOPwez/e6RrUb3o8ZvMr3hJl +Yy+6vEFEADIICfHSlIjkLJbGNFTRDccvkOPjD2W+fmzFAtWyNb/eqM+mwdTuXjOxTvP6V34zJsvc +YKJUzhhD8jI7GdqOoNoirTlaMVTH5udK0P2KvzD6F0LfwcOlc3bTvY9uI585xhdniK4yAIka8OMq +5zmyEQLYOadcVSscjAlkC1sQ0gbwL3AdwS+bntryq+2Ds380OJ+Z1Uy7TRkeBQIDAQABo4IDADCC +AvwwPAYJKwYBBAGCNxUHBC8wLQYlKwYBBAGCNxUI9/Bss4wDhbmBGISeqheH4YBfgSWC6qJEgcjE +IgIBZQIBKDATBgNVHSUEDDAKBggrBgEFBQcDBDAOBgNVHQ8BAf8EBAMCBaAwGwYJKwYBBAGCNxUK +BA4wDDAKBggrBgEFBQcDBDBEBgkqhkiG9w0BCQ8ENzA1MA4GCCqGSIb3DQMCAgIAgDAOBggqhkiG +9w0DBAICAIAwBwYFKw4DAgcwCgYIKoZIhvcNAwcwHQYDVR0OBBYEFDZHoDwoOKD5uzpF/2CcZSeg +XWLmMB8GA1UdIwQYMBaAFKHjMqgYYsgXIMCTfM26+6UW4LPrMIHVBgNVHR8Egc0wgcowgceggcSg +gcGGgb5sZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1DZWxlYnJpYW4sQ049Q0RQLENOPVB1 +YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQs +REM9bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENs +YXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIHHBggrBgEFBQcBAQSBujCBtzCBtAYIKwYBBQUHMAKG +gadsZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2Vy +dmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1hZCxEQz1udW1lcmljYSxEQz11 +cz9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTBS +BgNVHREESzBJoCwGCisGAQQBgjcUAgOgHgwcd2VzbGV5LnRheWxvckBhZC5udW1lcmljYS51c4EZ +d2VzbGV5LnRheWxvckBudW1lcmljYS51czANBgkqhkiG9w0BAQsFAAOCAQEAX3zFhiDYU+vQap2J +hiysyC9L7nkL7VI2OQWg4Z/JnNJTFiA6BwtoDYAT4qq1Jix4hZc+g78Gj99OnkhlBQDe9Hq12yI9 +muboQSDAYO6iDK76wQv3Rt8Fl4SUD4Ygwy52QrkTDrj/HZxTNask5p/2ilGBJnG9KT2VbEgGJkP9 +kXn1vAgOl3BCxgjdWekWCvxpmffr+Z3UtmQIiZAB3OsKcgdsSy9pveTMjxtKJemaH3kpXQiTgCev +CMuWZb3YnqXI8Fd+uUw6HwA4c+ZH62G9Q8KGkwXyhOPizmm3UeSlMo27yUCE+cF5EIHBxpGJ6z83 +7MbxMVKnS1Wz1n8MtW2ezDGCBCEwggQdAgEBMHMwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYK +CZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQBgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VM +RUJSSUFOLUNBAhMnAAALCTGsNkY7R2RVAAEAAAsJMA0GCWCGSAFlAwQCAwUAoIICfzAYBgkqhkiG +9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDA2MjUyMzIwNDRaME8GCSqGSIb3 +DQEJBDFCBEBaj66vdgjAhEO0p7lO6X44h+LpUlAcROa5Hi4Jp5aWS4hU8CuqOrH12y2GRNmNhKLa +0YieL4fCL3YqDRfop79NMFIGCyqGSIb3DQEJEAIBMUMwQQQdAAAAABAAAACgLzslsB99TKIYKeHy +Wh5cAQAAAACAAQAwHTAbgRl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIGCBgkrBgEEAYI3EAQx +dTBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYK +CZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklBTi1DQQITJwAACwkxrDZGO0dkVQAB +AAALCTCBhAYLKoZIhvcNAQkQAgsxdaBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT +8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklB +Ti1DQQITJwAACwkxrDZGO0dkVQABAAALCTCBkwYJKoZIhvcNAQkPMYGFMIGCMAsGCWCGSAFlAwQB +KjALBglghkgBZQMEARYwCgYIKoZIhvcNAwcwCwYJYIZIAWUDBAECMA4GCCqGSIb3DQMCAgIAgDAN +BggqhkiG9w0DAgIBQDALBglghkgBZQMEAgMwCwYJYIZIAWUDBAICMAsGCWCGSAFlAwQCATAHBgUr +DgMCGjANBgkqhkiG9w0BAQEFAASCAQBNFxhcbK6Rmw0Xyu+79cH5kUsXENcdUaJPKlegcY/gl2BZ +0CPpGcRnwz6z8OPYjvw3jrkiAE8nBbuCKu1CPtuk1h4Cybk7exyMybYvK5xge+N+dz2mFipRfGSY +rl/ztX1jyvcDruxaSJwb8WMhAGs505yfaCJfwgFOI3QGi+wUunbOIKy3QQZTXDv89yslZqi0wmeI +8sVRqSAYZRIPEylwS9CU2ReK9BJlfVLZnNP1At4gHE6S2hk8T0eVeLT8uhQiUXXJe4644UoPhoA4 +Fxgm7Q62KT6yP9O7c4eZzmQ4A9hdlWM6CtZ5pgMAzLOrVFdypzSc+S1j8DqcFkALCw83AAAAAAAA + +------=_NextPart_000_0018_01D64B14.F58791A0-- + +--===============0678627779074767862== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +_______________________________________________ +HTCondor-users mailing list +To unsubscribe, send a message to htcondor-users-request@cs.wisc.edu with a +subject: Unsubscribe +You can also unsubscribe by visiting +https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users + +The archives can be found at: +https://lists.cs.wisc.edu/archive/htcondor-users/ +--===============0678627779074767862==--)"; + + +static std::string +message(const std::regex& rx, size_t id) +{ + char buf[16]; + ::snprintf(buf, sizeof(buf), "%zu", id); + return std::regex_replace(test_msg, rx, buf); +} + +struct TestData { + size_t num_maildirs; + size_t num_messages; + size_t num_threads; +}; + + +static void +setup(const TestData& tdata) +{ + /* create toplevel */ + auto top_maildir = std::string{BENCH_MAILDIRS}; + int res = g_mkdir_with_parents(top_maildir.c_str(), 0700); + g_assert_cmpuint(res,==, 0); + + /* create maildirs */ + for (size_t i = 0; i != tdata.num_maildirs; ++i) { + const auto mdir = format("%s/maildir-%zu", top_maildir.c_str(), i); + auto res = maildir_mkdir(mdir); + g_assert(!!res); + } + const auto rx = std::regex("@ID@"); + /* create messages */ + for (size_t n = 0; n != tdata.num_messages; ++n) { + auto mpath = format("%s/maildir-%zu/cur/msg-%zu:2,S", + top_maildir.c_str(), + n % tdata.num_maildirs, + n); + std::ofstream stream(mpath); + auto msg = message(rx, n); + stream.write(msg.c_str(), msg.size()); + g_assert_true(stream.good()); + } +} + +static void +tear_down() +{ + /* ugly */ + GError *err{}; + const auto cmd{format("/bin/rm -rf '%s' '%s'", BENCH_MAILDIRS, BENCH_STORE)}; + if (!g_spawn_command_line_sync(cmd.c_str(), NULL, NULL, NULL, &err)) { + g_warning("error: %s\n", err ? err->message : "?"); + g_clear_error(&err); + } +} + +void +black_hole(void) +{ + return; /* do nothing */ +} + +static void +benchmark_indexer(gconstpointer testdata) +{ + using namespace std::chrono_literals; + using Clock = std::chrono::steady_clock; + const auto tdata = reinterpret_cast<const TestData*>(testdata); + + setup(*tdata); + auto start = Clock::now(); + + { + auto store{Store::make_new(BENCH_STORE, BENCH_MAILDIRS, {}, {})}; + g_assert_true(!!store); + Indexer::Config conf{}; + conf.max_threads = tdata->num_threads; + + auto res = store->indexer().start(conf); + g_assert_true(res); + while(store->indexer().is_running()) { + std::this_thread::sleep_for(100ms); + } + g_assert_cmpuint(store->size(),==, tdata->num_messages); + } + + const auto elapsed = Clock::now() - start; + std::cout << "indexed " << tdata->num_messages << " messages in " + << tdata->num_maildirs << " maildirs in " + << to_ms(elapsed) << "ms; " + << to_us(elapsed) / tdata->num_messages << " μs/message; " + << static_cast<size_t>(1000*tdata->num_messages / to_ms(elapsed)) + << " messages/s" + << " (" << tdata->num_threads << " thread(s))\n"; + + tear_down(); +} + +int +main(int argc, char *argv[]) +{ + size_t num_maildirs{}, num_messages{}; + g_test_init(&argc, &argv, nullptr); + if (g_test_perf()) { + num_maildirs = 20; + num_messages = 5000; + } else { + num_maildirs = 10; + num_messages = 1000; + } + + g_log_set_handler( + NULL, + (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), + (GLogFunc)black_hole, + NULL); + + + size_t thread_num{}; + const auto tnum = g_getenv("THREAD_NUM"); + if (tnum) + thread_num = ::strtol(tnum, NULL, 10); + + if (thread_num != 0) { + /* THREAD_NUM specified */ + static TestData tdata{num_maildirs, num_messages, thread_num}; + char *name = g_strdup_printf("/bench/indexer/%zu-cores", thread_num); + g_test_add_data_func(name, &tdata, benchmark_indexer); + g_free(name); + } else { + /* no THREAD_NUM specified */ + + const size_t hw_threads = std::thread::hardware_concurrency(); + + { + static TestData tdata{num_maildirs, num_messages, 1}; + g_test_add_data_func("/bench/indexer/1-core", &tdata, benchmark_indexer); + } + + if (hw_threads > 2) { + static TestData tdata{num_maildirs, num_messages, hw_threads/2}; + char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads/2); + g_test_add_data_func(name, &tdata, benchmark_indexer); + g_free(name); + } + + if (hw_threads > 1) { + static TestData tdata{num_maildirs, num_messages, hw_threads}; + char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads); + g_test_add_data_func(name, &tdata, benchmark_indexer); + g_free(name); + } + } + + tear_down(); + + return g_test_run(); +} diff --git a/lib/tests/cjk/cur/test1 b/lib/tests/cjk/cur/test1 new file mode 100644 index 0000000..1538790 --- /dev/null +++ b/lib/tests/cjk/cur/test1 @@ -0,0 +1,10 @@ +From: "Bob" <bob@builder.com> +Subject: CJK 1 +To: "Chase" <chase@ppatrol.org> +Date: Thu, 18 Nov 2021 08:35:34 +0200 +Message-Id: 112342343e9dfo.fsf@builder.com +User-Agent: mu4e 1.7.5; emacs 29.0.50 + + サーバがダウンしました + +https://github.com/djcb/mu/issues/1428 diff --git a/lib/tests/cjk/cur/test2 b/lib/tests/cjk/cur/test2 new file mode 100644 index 0000000..875bff5 --- /dev/null +++ b/lib/tests/cjk/cur/test2 @@ -0,0 +1,10 @@ +From: "Bob" <bob@builder.com> +Subject: CJK 2 +To: "Chase" <chase@ppatrol.org> +Date: Thu, 18 Nov 2021 08:35:34 +0200 +Message-Id: 271r2342343e9dfo.fsf@builder.com +User-Agent: mu4e 1.7.5; emacs 29.0.50 + + スポンサーシップ募集 + +https://github.com/djcb/mu/issues/1428 diff --git a/lib/tests/cjk/cur/test3 b/lib/tests/cjk/cur/test3 new file mode 100644 index 0000000..f0efe71 --- /dev/null +++ b/lib/tests/cjk/cur/test3 @@ -0,0 +1,10 @@ +From: "Bob" <bob@builder.com> +Subject: CJK 3 +To: "Chase" <chase@ppatrol.org> +Date: Thu, 18 Nov 2021 08:35:34 +0200 +Message-Id: 3871r2342343e9dfo.fsf@builder.com +User-Agent: mu4e 1.7.5; emacs 29.0.50 + + サービス開始について + +https://github.com/djcb/mu/issues/1428 diff --git a/lib/tests/cjk/cur/test4 b/lib/tests/cjk/cur/test4 new file mode 100644 index 0000000..2bad399 --- /dev/null +++ b/lib/tests/cjk/cur/test4 @@ -0,0 +1,10 @@ +From: "Bob" <bob@builder.com> +Subject: CJK 4 +To: "Chase" <chase@ppatrol.org> +Date: Thu, 18 Nov 2021 08:35:34 +0200 +Message-Id: 4871r2342343e9dfo.fsf@builder.com +User-Agent: mu4e 1.7.5; emacs 29.0.50 + + ショルダーバック + +https://github.com/djcb/mu/issues/1428 diff --git a/lib/tests/meson.build b/lib/tests/meson.build new file mode 100644 index 0000000..17a9b72 --- /dev/null +++ b/lib/tests/meson.build @@ -0,0 +1,81 @@ +## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# +# tests +# +test('test-maildir', + executable('test-maildir', + 'test-mu-maildir.cc', + install: false, + dependencies: [glib_dep, lib_mu_dep])) +test('test-msg', + executable('test-msg', + 'test-mu-msg.cc', + install: false, + dependencies: [glib_dep, lib_mu_dep])) +test('test-store', + executable('test-store', + 'test-mu-store.cc', + install: false, + dependencies: [glib_dep, lib_mu_dep])) +test('test-query', + executable('test-query', + 'test-query.cc', + install: false, + dependencies: [glib_dep, gmime_dep, lib_mu_dep])) + +test('test-tokenizer', + executable('test-tokenizer', + 'test-tokenizer.cc', + install: false, + dependencies: [glib_dep, lib_mu_dep])) + +test('test-parser', + executable('test-parser', + 'test-parser.cc', + install: false, + dependencies: [glib_dep, gmime_dep, lib_mu_dep])) + +test('test-store-query', + executable('test-store-query', + 'test-mu-store-query.cc', + install: false, + dependencies: [glib_dep, gmime_dep, lib_mu_dep])) +# +# benchmarks +# +bench_maildirs=join_paths(meson.current_build_dir(), 'maildirs') +bench_store=join_paths(meson.current_build_dir(), 'store') +bench_indexer_exe = executable( + 'bench-indexer', + 'bench-indexer.cc', + install:false, + cpp_args:['-DBENCH_MAILDIRS="' + bench_maildirs + '"', + '-DBENCH_STORE="' + bench_store + '"', + ], + dependencies: [lib_mu_dep, glib_dep]) + +benchmark('bench-indexer', bench_indexer_exe, args: ['-m', 'perf']) + +# +# below does _not_ pass; it is believed that it's a false alarm. +# https://gitlab.gnome.org/GNOME/glib/-/issues/2662 + +# also register benchmark as a normal test so it gets included for +# valgrind/helgrind etc. +# test('test-bench-indexer', bench_indexer_exe, +# args : ['-m', 'quick'], env: ['THREADNUM=16']) diff --git a/lib/tests/test-indexer.cc b/lib/tests/test-indexer.cc new file mode 100644 index 0000000..32e9ea3 --- /dev/null +++ b/lib/tests/test-indexer.cc @@ -0,0 +1,69 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <vector> +#include <glib.h> + +#include <iostream> +#include <sstream> +#include <unistd.h> + +#include "mu-indexer.hh" +#include "utils/mu-utils.hh" +#include "test-mu-common.h" + +using namespace Mu; + +static void +test_index_maildir() +{ + allow_warnings(); + + Store store{test_mu_common_get_random_tmpdir(), std::string{MU_TESTMAILDIR}}; + Indexer idx{Indexer::Config{}, store}; + + g_assert_true(idx.start()); + while (idx.is_running()) { + sleep(1); + } + + g_print("again!\n"); + + g_assert_true(idx.start()); + while (idx.is_running()) { + sleep(1); + } +} + +int +main(int argc, char* argv[]) +try { + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/indexer/index-maildir", test_index_maildir); + + return g_test_run(); + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} catch (...) { + std::cerr << "caught exception\n"; + return 1; +} diff --git a/lib/tests/test-mu-container.cc b/lib/tests/test-mu-container.cc new file mode 100644 index 0000000..4fb1939 --- /dev/null +++ b/lib/tests/test-mu-container.cc @@ -0,0 +1,80 @@ +/* +** Copyright (C) 2014 Jakub Sitnicki <jsitnicki@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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" +#include <glib.h> + +#include "utils/mu-test-utils.hh" +#include "mu-container.hh" + +static gboolean +container_has_children(const MuContainer* c) +{ + return c && c->child; +} + +static gboolean +container_is_sibling_of(const MuContainer* c, const MuContainer* sibling) +{ + const MuContainer* cur; + + for (cur = c; cur; cur = cur->next) { + if (cur == sibling) + return TRUE; + } + + return container_is_sibling_of(sibling, c); +} + +static void +test_mu_container_splice_children_when_parent_has_no_siblings(void) +{ + MuContainer *child, *parent, *root_set; + + child = mu_container_new(NULL, 0, "child"); + parent = mu_container_new(NULL, 0, "parent"); + parent = mu_container_append_children(parent, child); + + root_set = parent; + root_set = mu_container_splice_children(root_set, parent); + + g_assert(root_set != NULL); + g_assert(!container_has_children(parent)); + g_assert(container_is_sibling_of(root_set, child)); + + mu_container_destroy(parent); + mu_container_destroy(child); +} + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/mu-container/mu-container-splice-children-when-parent-has-no-siblings", + test_mu_container_splice_children_when_parent_has_no_siblings); + + g_log_set_handler( + NULL, + (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), + (GLogFunc)black_hole, + NULL); + + return g_test_run(); +} diff --git a/lib/tests/test-mu-maildir.cc b/lib/tests/test-mu-maildir.cc new file mode 100644 index 0000000..d3713f6 --- /dev/null +++ b/lib/tests/test-mu-maildir.cc @@ -0,0 +1,577 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <glib.h> +#include <glib/gstdio.h> + +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <vector> +#include <fstream> + +#include "utils/mu-test-utils.hh" +#include "mu-maildir.hh" +#include "utils/mu-result.hh" +#include "utils/mu-util.h" + +using namespace Mu; + +static void +test_maildir_mkdir_01(void) +{ + int i; + gchar * tmpdir, *mdir, *tmp; + const gchar* subs[] = {"tmp", "cur", "new"}; + + tmpdir = test_mu_common_get_random_tmpdir(); + mdir = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux"); + + g_assert_true(!!maildir_mkdir(mdir, 0755, FALSE)); + + for (i = 0; i != G_N_ELEMENTS(subs); ++i) { + gchar* dir; + + dir = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, subs[i]); + g_assert_cmpuint(g_access(dir, R_OK), ==, 0); + g_assert_cmpuint(g_access(dir, W_OK), ==, 0); + g_free(dir); + } + + tmp = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex"); + g_assert_cmpuint(g_access(tmp, F_OK), !=, 0); + + g_free(tmp); + g_free(tmpdir); + g_free(mdir); +} + +static void +test_maildir_mkdir_02(void) +{ + int i; + gchar * tmpdir, *mdir, *tmp; + const gchar* subs[] = {"tmp", "cur", "new"}; + + tmpdir = test_mu_common_get_random_tmpdir(); + mdir = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux"); + + g_assert_true(!!maildir_mkdir(mdir, 0755, TRUE)); + + for (i = 0; i != G_N_ELEMENTS(subs); ++i) { + gchar* dir; + + dir = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, subs[i]); + g_assert_cmpuint(g_access(dir, R_OK), ==, 0); + + g_assert_cmpuint(g_access(dir, W_OK), ==, 0); + g_free(dir); + } + + tmp = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex"); + g_assert_cmpuint(g_access(tmp, F_OK), ==, 0); + + g_free(tmp); + g_free(tmpdir); + g_free(mdir); +} + +static void +test_maildir_mkdir_03(void) +{ + int i; + gchar * tmpdir, *mdir, *tmp; + const gchar* subs[] = {"tmp", "cur", "new"}; + + tmpdir = test_mu_common_get_random_tmpdir(); + mdir = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux"); + + /* create part of the structure already... */ + { + gchar* dir; + dir = g_strdup_printf("%s%ccur", mdir, G_DIR_SEPARATOR); + g_assert_cmpuint(g_mkdir_with_parents(dir, 0755), ==, 0); + g_free(dir); + } + + /* this should still work */ + g_assert_true(!!maildir_mkdir(mdir, 0755, FALSE)); + + for (i = 0; i != G_N_ELEMENTS(subs); ++i) { + gchar* dir; + + dir = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, subs[i]); + g_assert_cmpuint(g_access(dir, R_OK), ==, 0); + g_assert_cmpuint(g_access(dir, W_OK), ==, 0); + g_free(dir); + } + + tmp = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex"); + g_assert_cmpuint(g_access(tmp, F_OK), !=, 0); + + g_free(tmp); + g_free(tmpdir); + g_free(mdir); +} + +static void +test_maildir_mkdir_04(void) +{ + gchar *tmpdir, *mdir; + + tmpdir = test_mu_common_get_random_tmpdir(); + mdir = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux"); + + /* create part of the structure already... */ + { + gchar* dir; + g_assert_cmpuint(g_mkdir_with_parents(mdir, 0755), ==, 0); + dir = g_strdup_printf("%s%ccur", mdir, G_DIR_SEPARATOR); + g_assert_cmpuint(g_mkdir_with_parents(dir, 0000), ==, 0); + g_free(dir); + } + + /* this should fail now, because cur is not read/writable */ + if (geteuid() != 0) + g_assert_false(!!maildir_mkdir(mdir, 0755, false)); + + g_free(tmpdir); + g_free(mdir); +} + +static gboolean +ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data) +{ + return FALSE; /* don't abort */ +} + +static void +test_maildir_mkdir_05(void) +{ + /* this must fail */ + g_test_log_set_fatal_handler((GTestLogFatalFunc)ignore_error, NULL); + + g_assert_false(!!maildir_mkdir({}, 0755, true)); +} + +[[maybe_unused]] static void +assert_matches_regexp(const char* str, const char* rx) +{ + if (!g_regex_match_simple(rx, str, (GRegexCompileFlags)0, (GRegexMatchFlags)0)) { + if (g_test_verbose()) + g_print("%s does not match %s", str, rx); + g_assert(0); + } +} + + +static void +test_determine_target_ok(void) +{ + struct TestCase { + std::string old_path; + std::string root_maildir; + std::string target_maildir; + Flags new_flags; + bool new_name; + std::string expected; + }; + const std::vector<TestCase> testcases = { + TestCase{ /* change some flags */ + "/home/foo/Maildir/test/cur/123456:2,FR", + "/home/foo/Maildir", + {}, + Flags::Seen | Flags::Passed, + false, + "/home/foo/Maildir/test/cur/123456:2,PS" + }, + + TestCase{ /* from cur -> new */ + "/home/foo/Maildir/test/cur/123456:2,FR", + "/home/foo/Maildir", + {}, + Flags::New, + false, + "/home/foo/Maildir/test/new/123456" + }, + + TestCase{ /* from new->cur */ + "/home/foo/Maildir/test/cur/123456", + "/home/foo/Maildir", + {}, + Flags::Seen | Flags::Flagged, + false, + "/home/foo/Maildir/test/cur/123456:2,FS" + }, + + TestCase{ /* change maildir */ + "/home/foo/Maildir/test/cur/123456:2,FR", + "/home/foo/Maildir", + "/test2", + Flags::Flagged | Flags::Replied, + false, + "/home/foo/Maildir/test2/cur/123456:2,FR" + }, + TestCase{ /* remove all flags */ + "/home/foo/Maildir/test/new/123456", + "/home/foo/Maildir", + {}, + Flags::None, + false, + "/home/foo/Maildir/test/cur/123456:2," + }, + }; + + for (auto&& testcase: testcases) { + const auto res = maildir_determine_target( + testcase.old_path, + testcase.root_maildir, + testcase.target_maildir, + testcase.new_flags, + testcase.new_name); + g_assert_true(!!res); + g_assert_cmpstr(testcase.expected.c_str(), ==, + res.value().c_str()); + } +} + + + +static void +test_determine_target_fail(void) +{ + struct TestCase { + std::string old_path; + std::string root_maildir; + std::string target_maildir; + Flags new_flags; + bool new_name; + std::string expected; + }; + const std::vector<TestCase> testcases = { + TestCase{ /* fail: no absolute path */ + "../foo/Maildir/test/cur/123456:2,FR-not-absolute", + "/home/foo/Maildir", + {}, + Flags::Seen | Flags::Passed, + false, + "/home/foo/Maildir/test/cur/123456:2,PS" + }, + + TestCase{ /* fail: no absolute root */ + "/home/foo/Maildir/test/cur/123456:2,FR", + "../foo/Maildir-not-absolute", + {}, + Flags::New, + false, + "/home/foo/Maildir/test/new/123456" + }, + + TestCase{ /* fail: maildir must start with '/' */ + "/home/foo/Maildir/test/cur/123456", + "/home/foo/Maildir", + "mymaildirwithoutslash", + Flags::Seen | Flags::Flagged, + false, + "/home/foo/Maildir/test/cur/123456:2,FS" + }, + + TestCase{ /* fail: path must be below maildir */ + "/home/foo/Maildir/test/cur/123456:2,FR", + "/home/bar/Maildir", + "/test2", + Flags::Flagged | Flags::Replied, + false, + "/home/foo/Maildir/test2/cur/123456:2,FR" + }, + TestCase{ /* fail: New cannot be combined */ + "/home/foo/Maildir/test/new/123456", + "/home/foo/Maildir", + {}, + Flags::New | Flags::Replied, + false, + "/home/foo/Maildir/test/cur/123456:2," + }, + }; + + for (auto&& testcase: testcases) { + const auto res = maildir_determine_target( + testcase.old_path, + testcase.root_maildir, + testcase.target_maildir, + testcase.new_flags, + testcase.new_name); + g_assert_false(!!res); + } +} + + + +static void +test_maildir_get_new_path_01(void) +{ + struct { + std::string oldpath; + Flags flags; + std::string newpath; + } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", + Flags::Replied, + "/home/foo/Maildir/test/cur/123456:2,R"}, + {"/home/foo/Maildir/test/cur/123456:2,FR", + Flags::New, + "/home/foo/Maildir/test/new/123456"}, + {"/home/foo/Maildir/test/new/123456:2,FR", + (Flags::Seen | Flags::Replied), + "/home/foo/Maildir/test/cur/123456:2,RS"}, + {"/home/foo/Maildir/test/new/1313038887_0.697", + (Flags::Seen | Flags::Flagged | Flags::Passed), + "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"}, + {"/home/foo/Maildir/test/new/1313038887_0.697:2,", + (Flags::Seen | Flags::Flagged | Flags::Passed), + "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"}, + /* note the ':2,' suffix on the new message is + * removed */ + + {"/home/foo/Maildir/trash/new/1312920597.2206_16.cthulhu", + Flags::Seen, + "/home/foo/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S"}}; + + for (int i = 0; i != G_N_ELEMENTS(paths); ++i) { + const auto newpath{maildir_determine_target(paths[i].oldpath, + "/home/foo/Maildir", + {}, paths[i].flags, false)}; + assert_valid_result(newpath); + assert_equal(*newpath, paths[i].newpath); + } +} + +static void +test_maildir_get_new_path_02(void) +{ + struct { + std::string oldpath; + Flags flags; + std::string targetdir; + std::string newpath; + std::string root_maildir; + } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", + Flags::Replied, + "/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,R", + "/home/foo/Maildir"}, + {"/home/bar/Maildir/test/cur/123456:2,FR", + Flags::New, + "/coffee", + "/home/bar/Maildir/coffee/new/123456", + "/home/bar/Maildir" + }, + {"/home/cuux/Maildir/test/new/123456", + (Flags::Seen | Flags::Replied), + "/tea", + "/home/cuux/Maildir/tea/cur/123456:2,RS", + "/home/cuux/Maildir"}, + {"/home/boy/Maildir/test/new/1313038887_0.697:2,", + (Flags::Seen | Flags::Flagged | Flags::Passed), + "/stuff", + "/home/boy/Maildir/stuff/cur/1313038887_0.697:2,FPS", + "/home/boy/Maildir"}}; + + for (int i = 0; i != G_N_ELEMENTS(paths); ++i) { + auto newpath{maildir_determine_target(paths[i].oldpath, + paths[i].root_maildir, + paths[i].targetdir, + paths[i].flags, + false)}; + assert_valid_result(newpath); + assert_equal(*newpath, paths[i].newpath); + } +} + +static void +test_maildir_get_new_path_custom(void) +{ + struct { + std::string oldpath; + Flags flags; + std::string targetdir; + std::string newpath; + std::string root_maildir; + } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", + Flags::Replied, + "/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,R", + "/home/foo/Maildir"}, + {"/home/foo/Maildir/test/cur/123456:2,hFeRllo123", + Flags::Flagged, + "/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,F", + "/home/foo/Maildir"}, + {"/home/foo/Maildir/test/cur/123456:2,abc", + Flags::Passed, + "/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,P", + "/home/foo/Maildir"}}; + + for (int i = 0; i != G_N_ELEMENTS(paths); ++i) { + auto newpath{maildir_determine_target(paths[i].oldpath, + paths[1].root_maildir, + paths[i].targetdir, + paths[i].flags, + false)}; + assert_valid_result(newpath); + assert_equal(*newpath, paths[i].newpath); + } +} + +static void +test_maildir_from_path(void) +{ + unsigned u; + + struct { + std::string path, exp; + } cases[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", "/test"}, + {"/home/foo/Maildir/lala/new/1313038887_0.697:2,", "/lala"}}; + + for (u = 0; u != G_N_ELEMENTS(cases); ++u) { + auto mdir{maildir_from_path(cases[u].path, "/home/foo/Maildir")}; + assert_valid_result(mdir); + assert_equal(*mdir, cases[u].exp); + } +} + +static void +test_maildir_link() +{ + TempDir tmpdir; + + assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo")); + assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar")); + + const auto srcpath1 = tmpdir.path() + "/foo/cur/msg1"; + const auto srcpath2 = tmpdir.path() + "/foo/new/msg2"; + + { + std::ofstream stream(srcpath1); + stream.write("cur", 3); + g_assert_true(stream.good()); + stream.close(); + } + + { + std::ofstream stream(srcpath2); + stream.write("new", 3); + g_assert_true(stream.good()); + stream.close(); + } + + assert_valid_result(maildir_link(srcpath1, tmpdir.path() + "/bar", false)); + assert_valid_result(maildir_link(srcpath2, tmpdir.path() + "/bar", false)); + + const auto dstpath1 = tmpdir.path() + "/bar/cur/msg1"; + const auto dstpath2 = tmpdir.path() + "/bar/new/msg2"; + + g_assert_true(g_access(dstpath1.c_str(), F_OK) == 0); + g_assert_true(g_access(dstpath2.c_str(), F_OK) == 0); + + assert_valid_result(maildir_clear_links(tmpdir.path() + "/bar")); + g_assert_false(g_access(dstpath1.c_str(), F_OK) == 0); + g_assert_false(g_access(dstpath2.c_str(), F_OK) == 0); +} + + +static void +test_maildir_move(bool use_gio) +{ + TempDir tmpdir; + + assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo")); + assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar")); + + const auto srcpath1 = tmpdir.path() + "/foo/cur/msg1"; + const auto srcpath2 = tmpdir.path() + "/foo/new/msg2"; + + { + std::ofstream stream(srcpath1); + stream.write("cur", 3); + g_assert_true(stream.good()); + stream.close(); + } + + { + std::ofstream stream(srcpath2); + stream.write("new", 3); + g_assert_true(stream.good()); + stream.close(); + } + + const auto dstpath = tmpdir.path() + "/test1"; + + assert_valid_result(maildir_move_message(srcpath1, dstpath, use_gio)); + assert_valid_result(maildir_move_message(srcpath2, dstpath, use_gio)); + + + //g_assert_true(g_access(dstpath.c_str(), F_OK) == 0); +} + +static void +test_maildir_move_vanilla() +{ + test_maildir_move(false/*!gio*/); +} + +static void +test_maildir_move_gio() +{ + test_maildir_move(true/*gio*/); +} + + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + /* mu_util_maildir_mkmdir */ + g_test_add_func("/mu-maildir/mu-maildir-mkdir-01", test_maildir_mkdir_01); + g_test_add_func("/mu-maildir/mu-maildir-mkdir-02", test_maildir_mkdir_02); + g_test_add_func("/mu-maildir/mu-maildir-mkdir-03", test_maildir_mkdir_03); + g_test_add_func("/mu-maildir/mu-maildir-mkdir-04", test_maildir_mkdir_04); + g_test_add_func("/mu-maildir/mu-maildir-mkdir-05", test_maildir_mkdir_05); + + g_test_add_func("/mu-maildir/mu-maildir-determine-target-ok", + test_determine_target_ok); + g_test_add_func("/mu-maildir/mu-maildir-determine-target-fail", + test_determine_target_fail); + + // /* get/set flags */ + g_test_add_func("/mu-maildir/mu-maildir-get-new-path-01", test_maildir_get_new_path_01); + g_test_add_func("/mu-maildir/mu-maildir-get-new-path-02", test_maildir_get_new_path_02); + g_test_add_func("/mu-maildir/mu-maildir-get-new-path-custom", + test_maildir_get_new_path_custom); + g_test_add_func("/mu-maildir/mu-maildir-from-path", + test_maildir_from_path); + + g_test_add_func("/mu-maildir/mu-maildir-link", test_maildir_link); + + g_test_add_func("/mu-maildir/mu-maildir-move-vanilla", test_maildir_move_vanilla); + g_test_add_func("/mu-maildir/mu-maildir-move-gio", test_maildir_move_gio); + + return g_test_run(); +} diff --git a/lib/tests/test-mu-msg-fields.cc b/lib/tests/test-mu-msg-fields.cc new file mode 100644 index 0000000..5f5df16 --- /dev/null +++ b/lib/tests/test-mu-msg-fields.cc @@ -0,0 +1,126 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> + +#include <locale.h> + +#include "utils/mu-test-utils.hh" +#include "mu-message-fields.hh" + +static void +test_mu_msg_field_body(void) +{ + Field::Id field; + + field = Field::Id::BodyText; + + g_assert_cmpstr(mu_msg_field_name(field), ==, "body"); + g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'b'); + g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'B'); + + g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE); +} + +static void +test_mu_msg_field_subject(void) +{ + Field::Id field; + + field = Field::Id::Subject; + + g_assert_cmpstr(mu_msg_field_name(field), ==, "subject"); + g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 's'); + g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'S'); + + g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE); +} + +static void +test_mu_msg_field_to(void) +{ + Field::Id field; + + field = Field::Id::To; + + g_assert_cmpstr(mu_msg_field_name(field), ==, "to"); + g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 't'); + g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'T'); + + g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE); +} + +static void +test_mu_msg_field_prio(void) +{ + Field::Id field; + + field = Field::Id::Priority; + + g_assert_cmpstr(mu_msg_field_name(field), ==, "prio"); + g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'p'); + g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'P'); + + g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, TRUE); +} + +static void +test_mu_msg_field_flags(void) +{ + Field::Id field; + + field = Field::Id::Flags; + + g_assert_cmpstr(mu_msg_field_name(field), ==, "flag"); + g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'g'); + g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'G'); + + g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, TRUE); +} + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + /* mu_msg_str_date */ + g_test_add_func("/mu-msg-fields/mu-msg-field-body", test_mu_msg_field_body); + g_test_add_func("/mu-msg-fields/mu-msg-field-subject", test_mu_msg_field_subject); + g_test_add_func("/mu-msg-fields/mu-msg-field-to", test_mu_msg_field_to); + g_test_add_func("/mu-msg-fields/mu-msg-field-prio", test_mu_msg_field_prio); + g_test_add_func("/mu-msg-fields/mu-msg-field-flags", test_mu_msg_field_flags); + + /* FIXME: add tests for mu_msg_str_flags; but note the + * function simply calls mu_msg_field_str */ + + g_log_set_handler( + NULL, + (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), + (GLogFunc)black_hole, + NULL); + + return g_test_run(); +} diff --git a/lib/tests/test-mu-msg.cc b/lib/tests/test-mu-msg.cc new file mode 100644 index 0000000..12a64ce --- /dev/null +++ b/lib/tests/test-mu-msg.cc @@ -0,0 +1,354 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include <glib.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> +#include <array> +#include <string> + +#include <locale.h> + +#include "utils/mu-test-utils.hh" +#include "utils/mu-result.hh" +#include "utils/mu-utils.hh" + +#include <message/mu-message.hh> + +using namespace Mu; + +using ExpectedContacts = const std::vector<std::pair<std::string, std::string>>; + +static void +assert_contacts_equal(const Contacts& contacts, + const ExpectedContacts& expected) +{ + g_assert_cmpuint(contacts.size(), ==, expected.size()); + + size_t n{}; + for (auto&& contact: contacts) { + if (g_test_verbose()) + g_message("{ \"%s\", \"%s\"},\n", contact.name.c_str(), contact.email.c_str()); + assert_equal(contact.name, expected.at(n).first); + assert_equal(contact.email, expected.at(n).second); + ++n; + } + g_print("\n"); +} + + +static void +test_mu_msg_01(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863042.12663_1.mindcrime!2,S") + .value()}; + + assert_contacts_equal(msg.to(), {{ "Donald Duck", "gcc-help@gcc.gnu.org" }}); + assert_contacts_equal(msg.from(), {{ "Mickey Mouse", "anon@example.com" }}); + + assert_equal(msg.subject(), "gcc include search order"); + assert_equal(msg.message_id(), + "3BE9E6535E3029448670913581E7A1A20D852173@" + "emss35m06.us.lmco.com"); + assert_equal(msg.header("Mailing-List").value_or(""), + "contact gcc-help-help@gcc.gnu.org; run by ezmlm"); + g_assert_true(msg.priority() == Priority::Normal); + g_assert_cmpuint(msg.date(), ==, 1217530645); + + assert_contacts_equal(msg.all_contacts(), { + { "", "gcc-help-owner@gcc.gnu.org"}, + { "Mickey Mouse", "anon@example.com" }, + { "Donald Duck", "gcc-help@gcc.gnu.org" } + }); + +} + +static void +test_mu_msg_02(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863087.12663_19.mindcrime!2,S") + .value()}; + + assert_equal(msg.to().at(0).email, "help-gnu-emacs@gnu.org"); + assert_equal(msg.subject(), "Re: Learning LISP; Scheme vs elisp."); + assert_equal(msg.from().at(0).email, "anon@example.com"); + assert_equal(msg.message_id(), "r6bpm5-6n6.ln1@news.ducksburg.com"); + assert_equal(msg.header("Errors-To").value_or(""), + "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org"); + g_assert_true(msg.priority() /* 'low' */ + == Priority::Low); + g_assert_cmpuint(msg.date(), ==, 1218051515); + g_print("flags: %s\n", Mu::to_string(msg.flags()).c_str()); + g_assert_true(msg.flags() == (Flags::Seen|Flags::MailingList)); + + assert_contacts_equal(msg.all_contacts(), { + { "", "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org"}, + { "", "anon@example.com"}, + { "", "help-gnu-emacs@gnu.org"}, + }); + +} + +static void +test_mu_msg_03(void) +{ + //const GSList* params; + + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1283599333.1840_11.cthulhu!2,") + .value()}; + + assert_equal(msg.to().at(0).display_name(), "Bilbo Baggins <bilbo@anotherexample.com>"); + assert_equal(msg.subject(), "Greetings from Lothlórien"); + assert_equal(msg.from().at(0).display_name(), "Frodo Baggins <frodo@example.com>"); + g_assert_true(msg.priority() == Priority::Normal); + g_assert_cmpuint(msg.date(), ==, 0); + assert_equal(msg.body_text().value_or(""), + "\nLet's write some fünkÿ text\nusing umlauts.\n\nFoo.\n"); + + // params = mu_msg_get_body_text_content_type_parameters(msg, MU_MSG_OPTION_NONE); + // g_assert_cmpuint(g_slist_length((GSList*)params), ==, 2); + + // assert_equal((char*)params->data, "charset"); + // params = g_slist_next(params); + // assert_equal((char*)params->data, "UTF-8"); + g_assert_true(msg.flags() == (Flags::Unread)); +} + +static void +test_mu_msg_04(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail5").value()}; + + assert_equal(msg.to().at(0).display_name(), "George Custer <gac@example.com>"); + assert_equal(msg.subject(), "pics for you"); + assert_equal(msg.from().at(0).display_name(), "Sitting Bull <sb@example.com>"); + g_assert_true(msg.priority() /* 'low' */ + == Priority::Normal); + g_assert_cmpuint(msg.date(), ==, 0); + g_assert_true(msg.flags() == + (Flags::HasAttachment|Flags::Unread)); + g_assert_true(msg.flags() == + (Flags::HasAttachment|Flags::Unread)); +} + +static void +test_mu_msg_multimime(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/multimime!2,FS").value()}; + + /* ie., are text parts properly concatenated? */ + assert_equal(msg.subject(), "multimime"); + assert_equal(msg.body_text().value_or(""), "abcdef"); + g_assert_true(msg.flags() == (Flags::HasAttachment|Flags::Flagged|Flags::Seen)); +} + +static void +test_mu_msg_flags(void) +{ + std::array<std::pair<std::string, Flags>, 2> tests= {{ + {MU_TESTMAILDIR4 "/multimime!2,FS", + (Flags::Flagged | Flags::Seen | + Flags::HasAttachment)}, + {MU_TESTMAILDIR4 "/special!2,Sabc", + (Flags::Seen)} + }}; + + for (auto&& test: tests) { + auto msg = Message::make_from_path(test.first); + assert_valid_result(msg); + g_assert_true(msg->flags() == test.second); + } +} + +static void +test_mu_msg_umlaut(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,") + .value()}; + + assert_contacts_equal(msg.to(), { { "Helmut Kröger", "hk@testmu.xxx"}}); + assert_contacts_equal(msg.from(), { { "Mü", "testmu@testmu.xx"}}); + + assert_equal(msg.subject(), "Motörhead"); + assert_equal(msg.from().at(0).display_name(), "Mü <testmu@testmu.xx>"); + g_assert_true(msg.priority() == Priority::Normal); + g_assert_cmpuint(msg.date(), ==, 0); +} + +static void +test_mu_msg_references(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,") + .value()}; + + std::array<std::string, 4> expected_refs = { + "non-exist-01@msg.id", + "non-exist-02@msg.id", + "non-exist-03@msg.id", + "non-exist-04@msg.id" + }; + + assert_equal_seq_str(msg.references(), expected_refs); + assert_equal(msg.thread_id(), expected_refs[0]); +} + +static void +test_mu_msg_references_dups(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1252168370_3.14675.cthulhu!2,S") + .value()}; + + std::array<std::string, 6> expected_refs = { + "439C1136.90504@euler.org", + "4399DD94.5070309@euler.org", + "20051209233303.GA13812@gauss.org", + "439B41ED.2080402@euler.org", + "439A1E03.3090604@euler.org", + "20051211184308.GB13513@gauss.org" + }; + + assert_equal_seq_str(msg.references(), expected_refs); + assert_equal(msg.thread_id(), expected_refs[0]); +} + +static void +test_mu_msg_references_many(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR2 "/bar/cur/181736.eml") + .value()}; + + std::array<std::string, 11> expected_refs = { + "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com", + "87hbblwelr.fsf@sapphire.mobileactivedefense.com", + "pql248-4va.ln1@wilbur.25thandClement.com", + "ikns6r$li3$1@Iltempo.Update.UU.SE", + "8762s0jreh.fsf@sapphire.mobileactivedefense.com", + "ikqqp1$jv0$1@Iltempo.Update.UU.SE", + "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com", + "ikr0na$lru$1@Iltempo.Update.UU.SE", + "tO8cp.1228$GE6.370@news.usenetserver.com", + "ikr6ks$nlf$1@Iltempo.Update.UU.SE", + "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk" + }; + + assert_equal_seq_str(msg.references(), expected_refs); + assert_equal(msg.thread_id(), expected_refs[0]); +} + +static void +test_mu_msg_tags(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail1").value()}; + + assert_contacts_equal(msg.to(), {{ "Julius Caesar", "jc@example.com" }}); + assert_contacts_equal(msg.from(), {{ "John Milton", "jm@example.com" }}); + + assert_equal(msg.subject(),"Fere libenter homines id quod volunt credunt"); + + g_assert_true(msg.priority() == Priority::High); + g_assert_cmpuint(msg.date(), ==, 1217530645); + + std::array<std::string, 4> expected_tags = { + "Paradise", + "losT", + "john", + "milton" + }; + assert_equal_seq_str(msg.tags(), expected_tags); +} + +static void +test_mu_msg_comp_unix_programmer(void) +{ + auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/181736.eml").value()}; + + g_assert_true(msg.to().empty()); + assert_equal(msg.subject(), + "Re: Are writes \"atomic\" to readers of the file?"); + assert_equal(msg.from().at(0).display_name(), "Jimbo Foobarcuux <jimbo@slp53.sl.home>"); + assert_equal(msg.message_id(), "oktdp.42997$Te.22361@news.usenetserver.com"); + + auto refs = join(msg.references(), ','); + assert_equal(refs, + "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt" + ".googlegroups.com," + "87hbblwelr.fsf@sapphire.mobileactivedefense.com," + "pql248-4va.ln1@wilbur.25thandClement.com," + "ikns6r$li3$1@Iltempo.Update.UU.SE," + "8762s0jreh.fsf@sapphire.mobileactivedefense.com," + "ikqqp1$jv0$1@Iltempo.Update.UU.SE," + "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com," + "ikr0na$lru$1@Iltempo.Update.UU.SE," + "tO8cp.1228$GE6.370@news.usenetserver.com," + "ikr6ks$nlf$1@Iltempo.Update.UU.SE," + "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk"); + + //"jimbo@slp53.sl.home (Jimbo Foobarcuux)"; + g_assert_true(msg.priority() == Priority::Normal); + g_assert_cmpuint(msg.date(), ==, 1299603860); +} + +static void +test_mu_str_prio_01(void) +{ + g_assert_true(priority_name(Priority::Low) == "low"); + g_assert_true(priority_name(Priority::Normal) == "normal"); + g_assert_true(priority_name(Priority::High) == "high"); +} + +G_GNUC_UNUSED static gboolean +ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data) +{ + return FALSE; /* don't abort */ +} + + +int +main(int argc, char* argv[]) +{ + int rv; + + g_test_init(&argc, &argv, NULL); + + /* mu_msg_str_date */ + g_test_add_func("/mu-msg/mu-msg-01", test_mu_msg_01); + g_test_add_func("/mu-msg/mu-msg-02", test_mu_msg_02); + g_test_add_func("/mu-msg/mu-msg-03", test_mu_msg_03); + g_test_add_func("/mu-msg/mu-msg-04", test_mu_msg_04); + g_test_add_func("/mu-msg/mu-msg-multimime", test_mu_msg_multimime); + + g_test_add_func("/mu-msg/mu-msg-flags", test_mu_msg_flags); + + g_test_add_func("/mu-msg/mu-msg-tags", test_mu_msg_tags); + g_test_add_func("/mu-msg/mu-msg-references", test_mu_msg_references); + g_test_add_func("/mu-msg/mu-msg-references_dups", test_mu_msg_references_dups); + g_test_add_func("/mu-msg/mu-msg-references_many", test_mu_msg_references_many); + + g_test_add_func("/mu-msg/mu-msg-umlaut", test_mu_msg_umlaut); + g_test_add_func("/mu-msg/mu-msg-comp-unix-programmer", test_mu_msg_comp_unix_programmer); + + g_test_add_func("/mu-str/mu-str-prio-01", test_mu_str_prio_01); + + rv = g_test_run(); + + return rv; +} diff --git a/lib/tests/test-mu-store-query.cc b/lib/tests/test-mu-store-query.cc new file mode 100644 index 0000000..c22cc59 --- /dev/null +++ b/lib/tests/test-mu-store-query.cc @@ -0,0 +1,648 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#include "utils/mu-result.hh" +#include <array> +#include <thread> +#include <string> +#include <string_view> +#include <fstream> +#include <unordered_map> + +#include <mu-store.hh> +#include <mu-maildir.hh> +#include <utils/mu-utils.hh> +#include <utils/mu-test-utils.hh> +#include <message/mu-message.hh> + +using namespace Mu; + + +/// map of some (unique) path-tail to the message-text +using TestMap = std::unordered_map<std::string, std::string>; + +static Store +make_test_store(const std::string& test_path, const TestMap& test_map, + const StringVec &personal_addresses) +{ + std::string maildir = test_path + "/Maildir"; + + /* write messages to disk */ + for (auto&& item: test_map) { + + const auto msgpath = maildir + "/" + item.first; + + /* create the directory for the message */ + auto dir = to_string_gchar(g_path_get_dirname(msgpath.c_str())); + if (g_test_verbose()) + g_message("create message dir %s", dir.c_str()); + + g_assert_cmpuint(g_mkdir_with_parents(dir.c_str(), 0700), ==, 0); + + /* write the file */ + std::ofstream stream(msgpath); + stream.write(item.second.data(), item.second.size()); + g_assert_true(stream.good()); + stream.close(); + } + + /* make the store */ + auto store = Store::make_new(test_path, maildir, personal_addresses, {}); + assert_valid_result(store); + + /* index the messages */ + auto res = store->indexer().start({}); + g_assert_true(res); + while(store->indexer().is_running()) { + using namespace std::chrono_literals; + std::this_thread::sleep_for(100ms); + } + + if (test_map.size() > 0) + g_assert_false(store->empty()); + + g_assert_cmpuint(store->size(),==,test_map.size()); + + /* and we have a fully-ready store */ + return std::move(store.value()); +} + + +static void +test_simple() +{ + const TestMap test_msgs = {{ + +// "sqlite-msg" "Simple mailing list message. +{ +"basic/cur/sqlite-msg:2,S", +R"(Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST) +Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: "Foo Example" <foo@example.com> +To: sqlite-dev@sqlite.org +Cc: "Bank of America" <bank@example.com> +Bcc: Aku Ankka <donald.duck@duckstad.nl> +Mime-Version: 1.0 (Apple Message framework v926) +Date: Mon, 4 Aug 2008 11:40:49 +0200 +X-Mailer: Apple Mail (2.926) +Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: sqlite-dev-bounces@sqlite.org + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; + +I said: "Aujourd'hui!" +)"}, +}}; + TempDir tdir; + auto store{make_test_store(tdir.path(), test_msgs, {})}; + + // matches + for (auto&& expr: { + "Inside", + "from:foo@example.com", + "from:Foo", + "from:\"Foo Example\"", + "from:/Foo.*Example/", + "recip:\"Bank Of America\"", + "cc:bank@example.com", + "cc:bank", + "cc:america", + "bcc:donald.duck@duckstad.nl", + "bcc:donald.duck", + "bcc:duckstad.nl", + "bcc:aku", + "bcc:ankka", + "bcc:\"aku ankka\"", + "date:2008-08-01..2008-09-01", + "prio:low", + "to:sqlite-dev@sqlite.org", + "list:sqlite-dev.sqlite.org", + "aujourd'hui", + }) { + + if (g_test_verbose()) + g_message("query: '%s'", expr); + auto qr = store.run_query(expr); + assert_valid_result(qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 1); + } + + auto qr = store.run_query("statement"); + assert_valid_result(qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 1); + + assert_equal(qr->begin().subject().value_or(""), + "[sqlite-dev] VM optimization inside sqlite3VdbeExec"); + g_assert_true(qr->begin().references().empty()); + //g_assert_cmpuint(qr->begin().date().value_or(0), ==, 123454); +} + +static void +test_spam_address_components() +{ + const TestMap test_msgs = {{ + +// "sqlite-msg" "Simple mailing list message. +{ +"spam/cur/spam-msg:2,S", +R"(Message-Id: <abcde@foo.bar> +From: "Foo Example" <bar@example.com> +To: example@example.com +Subject: ***SPAM*** this is a test + +Boo! +)"}, +}}; + TempDir tdir; + auto store{make_test_store(tdir.path(), test_msgs, {})}; + + g_test_bug("2278"); + g_test_bug("2281"); + + // matches both + for (auto&& expr: { + "SPAM", + "spam", + "/.*SPAM.*/", + "subject:SPAM", + "from:bar@example.com", + "subject:\\*\\*\\*SPAM\\*\\*\\*", + "bar", + "example.com" + }) { + + if (g_test_verbose()) + g_message("query: '%s'", expr); + auto qr = store.run_query(expr); + assert_valid_result(qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 1); + } +} + + +static void +test_dups_related() +{ + const TestMap test_msgs = {{ +/* parent */ +{ +"inbox/cur/msg1:2,S", +R"(Message-Id: <abcde@foo.bar> +From: "Foo Example" <bar@example.com> +Date: Sat, 06 Aug 2022 11:01:54 -0700 +To: example@example.com +Subject: test1 + +Parent +)"}, +/* child (dup vv) */ +{ +"boo/cur/msg2:1,S", +R"(Message-Id: <edcba@foo.bar> +In-Reply-To: <abcde@foo.bar> +From: "Foo Example" <bar@example.com> +Date: Sat, 06 Aug 2022 13:01:54 -0700 +To: example@example.com +Subject: Re: test1 + +Child +)"}, +/* child (dup ^^) */ +{ +"inbox/cur/msg2:1,S", +R"(Message-Id: <edcba@foo.bar> +In-Reply-To: <abcde@foo.bar> +From: "Foo Example" <bar@example.com> +Date: Sat, 06 Aug 2022 14:01:54 -0700 +To: example@example.com +Subject: Re: test1 + +Child +)"}, +}}; + TempDir tdir; + auto store{make_test_store(tdir.path(), test_msgs, {})}; + { + // direct matches + auto qr = store.run_query("test1", Field::Id::Date, + QueryFlags::None); + g_assert_true(!!qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 3); + } + + { + // skip duplicate messages; which one is skipped is arbitrary. + auto qr = store.run_query("test1", Field::Id::Date, + QueryFlags::SkipDuplicates); + g_assert_true(!!qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 2); + } + + { + // no related + auto qr = store.run_query("Parent", Field::Id::Date); + g_assert_true(!!qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 1); + } + + { + // find related messages + auto qr = store.run_query("Parent", Field::Id::Date, + QueryFlags::IncludeRelated); + g_assert_true(!!qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 3); + } + + { + // find related messages, skip dups. the leader message + // should _not_ be skipped. + auto qr = store.run_query("test1 AND maildir:/inbox", + Field::Id::Date, + QueryFlags::IncludeRelated| + QueryFlags::SkipDuplicates); + g_assert_true(!!qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 2); + + // ie the /boo is to be skipped, since it's not in the leader + // set. + for (auto&& m: *qr) + assert_equal(m.message()->maildir(), "/inbox"); + } + + { + // find related messages, find parent from child. + auto qr = store.run_query("Child and maildir:/inbox", + Field::Id::Date, + QueryFlags::IncludeRelated); + g_assert_true(!!qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 3); + + } + + { + // find related messages, find parent from child. + // leader message wins + auto qr = store.run_query("Child and maildir:/inbox", + Field::Id::Date, + QueryFlags::IncludeRelated| + QueryFlags::SkipDuplicates| + QueryFlags::Descending); + g_assert_true(!!qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 2); + + // ie the /boo is to be skipped, since it's not in the leader + // set. + for (auto&& m: *qr) + assert_equal(m.message()->maildir(), "/inbox"); + } +} + + +static void +test_related_missing_root() +{ + const TestMap test_msgs = {{ +{ +"inbox/cur/msg1:2,S", +R"(Content-Type: text/plain; charset=utf-8 +References: <EZrZOnVCsYfFcX3Ls0VFoRnJdCGV4GM5YtO739l-iOB2ADNH7cIJWb0DaO5Of3BWDUEKq18Rz3a7rNoI96bNwQ==@protonmail.internalid> +To: "Joerg Roedel" <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com> +Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com> +From: "Dan Carpenter" <dan.carpenter@oracle.com> +Subject: [PATCH] iommu/omap: fix buffer overflow in debugfs +Date: Thu, 4 Aug 2022 17:32:39 +0300 +Message-Id: <YuvYh1JbE3v+abd5@kili> +List-Id: <kernel-janitors.vger.kernel.org> +Precedence: bulk + +There are two issues here: +)"}, +{ +"inbox/cur/msg2:2,S", +R"(Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=utf-8 +References: <YuvYh1JbE3v+abd5@kili> + <9pEUi_xoxa7NskF7EK_qfrlgjXzGsyw9K7cMfYbo-KI6fnyVMKTpc8E2Fu94V8xedd7cMpn0LlBrr9klBMflpw==@protonmail.internalid> +Reply-To: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com> +From: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com> +Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs +List-Id: <kernel-janitors.vger.kernel.org> +Message-Id: <YuvzKJM66k+ZPD9c@pendragon.ideasonboard.com> +Precedence: bulk +In-Reply-To: <YuvYh1JbE3v+abd5@kili> + +Hi Dan, + +Thank you for the patch. +)"}, +{ +"inbox/cur/msg3:2,S", +R"(Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=utf-8 +References: <YuvYh1JbE3v+abd5@kili> + <G6TStg8J52Q-uSMTR7wRQdPeloxpZMiEQT_F8_JIDYM25eEPeHGgrNKO0fuO78MiQgD9Mz4BDtsZlZgmPKFe4Q==@protonmail.internalid> +To: "Dan Carpenter" <dan.carpenter@oracle.com>, "Joerg Roedel" + <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com> +Reply-To: "Robin Murphy" <robin.murphy@arm.com> +From: "Robin Murphy" <robin.murphy@arm.com> +Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs +List-Id: <kernel-janitors.vger.kernel.org> +Message-Id: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> +Precedence: bulk +In-Reply-To: <YuvYh1JbE3v+abd5@kili> +Date: Thu, 4 Aug 2022 17:31:39 +0100 + +On 04/08/2022 3:32 pm, Dan Carpenter wrote: +> There are two issues here: +)"}, +{ +"inbox/new/msg4", +R"(Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=utf-8 +References: <YuvYh1JbE3v+abd5@kili> + <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> + <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid> +To: "Robin Murphy" <robin.murphy@arm.com> +Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com> +From: "Dan Carpenter" <dan.carpenter@oracle.com> +Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs +List-Id: <kernel-janitors.vger.kernel.org> +Date: Fri, 5 Aug 2022 09:37:02 +0300 +In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> +Precedence: bulk +Message-Id: <20220805063702.GH3438@kadam> + +On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote: +> On 04/08/2022 3:32 pm, Dan Carpenter wrote: +> > There are two issues here: +)"}, +}}; + TempDir tdir; + auto store{make_test_store(tdir.path(), test_msgs, {})}; + { + auto qr = store.run_query("fix buffer overflow in debugfs", + Field::Id::Date, QueryFlags::IncludeRelated); + g_assert_true(!!qr); + g_assert_cmpuint(qr->size(), ==, 4); + } + + { + auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread", + Field::Id::Date, QueryFlags::None); + g_assert_true(!!qr); + g_assert_cmpuint(qr->size(), ==, 1); + assert_equal(qr->begin().message_id().value_or(""), "20220805063702.GH3438@kadam"); + assert_equal(qr->begin().thread_id().value_or(""), "YuvYh1JbE3v+abd5@kili"); + } + + { + /* this one failed earlier, because the 'protonmail' id is the + * first reference, which means it does _not_ have the same + * thread-id as the rest; however, we filter these + * fake-message-ids now.*/ + g_test_bug("2312"); + + auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread", + Field::Id::Date, QueryFlags::IncludeRelated); + g_assert_true(!!qr); + g_assert_cmpuint(qr->size(), ==, 4); + } +} + + +static void +test_body_matricula() +{ + const TestMap test_msgs = {{ +{ +"basic/cur/matricula-msg:2,S", +R"(From: XXX <XX@XX.com> +Subject: + =?iso-8859-1?Q?EF_-_Pago_matr=EDcula_de_la_matr=EDcula_de_inscripci=F3n_a?= +Date: Thu, 4 Aug 2022 14:29:41 +0000 +Message-ID: + <VE1PR03MB5471882920DE08CFE44D97A0FE9F9@VE1PR03MB5471.eurprd03.prod.outlook.com> +Accept-Language: es-AR, es-ES, en-US +Content-Language: es-AR +X-MS-Has-Attach: yes +Content-Type: multipart/mixed; + boundary="_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_" +MIME-Version: 1.0 +X-OriginatorOrg: ef.com +X-MS-Exchange-CrossTenant-AuthAs: Internal +X-MS-Exchange-CrossTenant-AuthSource: VE1PR03MB5471.eurprd03.prod.outlook.com + +--_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_ +Content-Type: multipart/alternative; + boundary="_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_" + +--_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Buenas tardes Familia, + + +Espero que est=E9n muy bien. + + + +Ya cargamos en sistema su pre inscripci=F3n para el curso + + +Quedamos atentos ante cualquier consulta que surja. + +Saludos, +)"}, +}}; + TempDir tdir; + auto store{make_test_store(tdir.path(), test_msgs, {})}; + + /* i.e., non-utf8 text parts were not converted */ + g_test_bug("2333"); + + // matches + for (auto&& expr: { + "subject:matrícula", + "subject:matricula", + "body:atentos", + "body:inscripción" + }) { + + if (g_test_verbose()) + g_message("query: '%s'", expr); + auto qr = store.run_query(expr); + assert_valid_result(qr); + g_assert_false(qr->empty()); + g_assert_cmpuint(qr->size(), ==, 1); + } +} + + + +static void +test_duplicate_refresh_real(bool rename) +{ + g_test_bug("2327"); + + const TestMap test_msgs = {{ + "inbox/new/msg", + { R"(Message-Id: <abcde@foo.bar> +From: "Foo Example" <bar@example.com> +Date: Wed, 26 Oct 2022 11:01:54 -0700 +To: example@example.com +Subject: Rainy night in Helsinki + +Boo! +)"}, + }}; + + /* create maildir with message */ + TempDir tdir; + auto store{make_test_store(tdir.path(), test_msgs, {})}; + g_debug("%s", store.properties().root_maildir.c_str()); + /* ensure we have a proper maildir, with new/, cur/ */ + auto mres = maildir_mkdir(store.properties().root_maildir + "/inbox"); + assert_valid_result(mres); + g_assert_cmpuint(store.size(), ==, 1U); + + /* + * find the one msg with a query + */ + auto qr = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None); + g_assert_true(!!qr); + g_assert_cmpuint(qr->size(), ==, 1); + const auto old_path = qr->begin().path().value(); + const auto old_docid = qr->begin().doc_id(); + assert_equal(qr->begin().message()->path(), old_path); + g_assert_true(::access(old_path.c_str(), F_OK) == 0); + + /* + * mark as read, i.e. move to cur/; ensure it really moved. + */ + auto moved_msg = store.move_message(old_docid, Nothing, Flags::Seen, rename); + assert_valid_result(moved_msg); + const auto new_path = moved_msg->path(); + if (!rename) + assert_equal(new_path, store.properties().root_maildir + "/inbox/cur/msg:2,S"); + g_assert_cmpuint(store.size(), ==, 1); + g_assert_false(::access(old_path.c_str(), F_OK) == 0); + g_assert_true(::access(new_path.c_str(), F_OK) == 0); + + /* also ensure thath the cached sexp for the message has been updated; + * that's what mu4e uses */ + const auto moved_sexp{moved_msg->to_sexp().to_sexp_string()}; + /* clumsy */ + g_assert_true(moved_sexp.find(new_path) != std::string::npos); + + /* + * find new message with query, ensure it's really that new one. + */ + auto qr2 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None); + g_assert_true(!!qr2); + g_assert_cmpuint(qr2->size(), ==, 1); + assert_equal(qr2->begin().path().value(), new_path); + + /* index the messages */ + auto res = store.indexer().start({}); + g_assert_true(res); + while(store.indexer().is_running()) { + using namespace std::chrono_literals; + std::this_thread::sleep_for(100ms); + } + g_assert_cmpuint(store.size(), ==, 1); + + /* + * ensure query still has the right results + */ + auto qr3 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None); + g_assert_true(!!qr3); + g_assert_cmpuint(qr3->size(), ==, 1); + const auto path3{qr3->begin().path().value()}; + assert_equal(path3, new_path); + assert_equal(qr3->begin().message()->path(), new_path); + g_assert_true(::access(path3.c_str(), F_OK) == 0); +} + + +static void +test_duplicate_refresh() +{ + test_duplicate_refresh_real(false/*no rename*/); +} + + +static void +test_duplicate_refresh_rename() +{ + test_duplicate_refresh_real(true/*rename*/); +} + + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_bug_base("https://github.com/djcb/mu/issues/"); + + g_test_add_func("/store/query/simple", test_simple); + g_test_add_func("/store/query/spam-address-components", + test_spam_address_components); + g_test_add_func("/store/query/dups-related", + test_dups_related); + g_test_add_func("/store/query/related-missing-root", + test_related_missing_root); + g_test_add_func("/store/query/body-matricula", + test_body_matricula); + g_test_add_func("/store/query/duplicate-refresh", + test_duplicate_refresh); + g_test_add_func("/store/query/duplicate-refresh-rename", + test_duplicate_refresh_rename); + + return g_test_run(); +} diff --git a/lib/tests/test-mu-store.cc b/lib/tests/test-mu-store.cc new file mode 100644 index 0000000..1a140e3 --- /dev/null +++ b/lib/tests/test-mu-store.cc @@ -0,0 +1,365 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include <glib.h> +#include <stdlib.h> +#include <thread> +#include <unistd.h> +#include <time.h> +#include <fstream> + +#include <locale.h> + +#include "utils/mu-test-utils.hh" +#include "mu-store.hh" +#include "utils/mu-result.hh" +#include <utils/mu-utils.hh> +#include "mu-maildir.hh" + +using namespace Mu; + +static std::string MuTestMaildir = Mu::canonicalize_filename(MU_TESTMAILDIR, "/"); +static std::string MuTestMaildir2 = Mu::canonicalize_filename(MU_TESTMAILDIR2, "/"); + +static void +test_store_ctor_dtor() +{ + TempDir tempdir; + auto store{Store::make_new(tempdir.path(), "/tmp", {}, {})}; + assert_valid_result(store); + + g_assert_true(store->empty()); + g_assert_cmpuint(0, ==, store->size()); + + g_assert_cmpstr(MU_STORE_SCHEMA_VERSION, ==, + store->properties().schema_version.c_str()); +} + +static void +test_store_add_count_remove() +{ + TempDir tempdir{false}; + + auto store{Store::make_new(tempdir.path() + "/xapian", MuTestMaildir, {}, {})}; + assert_valid_result(store); + + const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"}; + const auto id1 = store->add_message(msgpath); + assert_valid_result(id1); + store->commit(); + + g_assert_cmpuint(store->size(), ==, 1); + g_assert_true(store->contains_message(msgpath)); + + g_assert_true(store->contains_message(msgpath)); + + const auto id2 = store->add_message(MuTestMaildir2 + "/bar/cur/mail3"); + g_assert_false(!!id2); // wrong maildir. + store->commit(); + + const auto msg3path{MuTestMaildir + "/cur/1252168370_3.14675.cthulhu!2,S"}; + const auto id3 = store->add_message(msg3path); + assert_valid_result(id3); + + g_assert_cmpuint(store->size(), ==, 2); + g_assert_true(store->contains_message(msg3path)); + + store->remove_message(id1.value()); + g_assert_cmpuint(store->size(), ==, 1); + g_assert_false( + store->contains_message(MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,")); + + store->remove_message(msg3path); + g_assert_true(store->empty()); + g_assert_false(store->contains_message(msg3path)); +} + + +static void +test_message_mailing_list() +{ + constexpr const char *test_message_1 = +R"(Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST) +Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: anon@example.com +To: sqlite-dev@sqlite.org +Mime-Version: 1.0 (Apple Message framework v926) +Date: Mon, 4 Aug 2008 11:40:49 +0200 +X-Mailer: Apple Mail (2.926) +Subject: Capybaras United +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: sqlite-dev-bounces@sqlite.org +Content-Length: 639 + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; +)"; + TempDir tempdir; + auto store{Store::make_new(tempdir.path(), "/home/test/Maildir", {}, {})}; + assert_valid_result(store); + + const auto msgpath{"/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S"}; + auto message{Message::make_from_text(test_message_1, msgpath)}; + assert_valid_result(message); + + const auto docid = store->add_message(*message); + assert_valid_result(docid); + g_assert_cmpuint(store->size(),==, 1); + + /* ensure 'update' dtrt, i.e., nothing. */ + const auto docid2 = store->update_message(*message, *docid); + assert_valid_result(docid2); + g_assert_cmpuint(store->size(),==, 1); + g_assert_cmpuint(*docid,==,*docid2); + + auto msg2{store->find_message(*docid)}; + g_assert_true(!!msg2); + assert_equal(message->path(), msg2->path()); + + g_assert_true(store->contains_message(message->path())); + + const auto qr = store->run_query("to:sqlite-dev@sqlite.org"); + g_assert_true(!!qr); + g_assert_cmpuint(qr->size(), ==, 1); +} + + +static void +test_message_attachments(void) +{ + constexpr const char* msg_text = +R"(Return-Path: <foo@example.com> +Received: from pop.gmail.com [256.85.129.309] + by evergrey with POP3 (fetchmail-6.4.29) + for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET) +Sender: "Foo, Example" <foo@example.com> +User-agent: mu4e 1.7.11; emacs 29.0.50 +From: "Foo Example" <foo@example.com> +To: bar@example.com +Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?= +Date: Thu, 24 Mar 2022 20:04:39 +0200 +Organization: ACME Inc. +Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM> +MIME-Version: 1.0 +X-label: @NextActions operation:mindcrime Queensrÿche +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain + +Hello, +--=-=-= +Content-Type: image/jpeg +Content-Disposition: attachment; filename=file-01.bin +Content-Transfer-Encoding: base64 + +AAECAw== +--=-=-= +Content-Type: audio/ogg +Content-Disposition: inline; filename=/tmp/file-02.bin +Content-Transfer-Encoding: base64 + +BAUGBw== +--=-=-= +Content-Type: message/rfc822 +Content-Disposition: attachment; + filename="message.eml" + +From: "Fnorb" <fnorb@example.com> +To: Bob <bob@example.com> +Subject: news for you +Date: Mon, 28 Mar 2022 22:53:26 +0300 + +Attached message! + +--=-=-= +Content-Type: text/plain + +World! +--=-=-=-- +)"; + + TempDir tempdir; + auto store{Store::make_new(tempdir.path(), "/home/test/Maildir", {}, {})}; + assert_valid_result(store); + + auto message{Message::make_from_text( + msg_text, + "/home/test/Maildir/inbox/cur/1649279256.abcde_1.evergrey:2,S")}; + assert_valid_result(message); + + const auto docid = store->add_message(*message); + assert_valid_result(docid); + store->commit(); + + auto msg2{store->find_message(*docid)}; + g_assert_true(!!msg2); + assert_equal(message->path(), msg2->path()); + + g_assert_true(store->contains_message(message->path())); + + // for (auto&& term = msg2->document().xapian_document().termlist_begin(); + // term != msg2->document().xapian_document().termlist_end(); ++term) + // g_message(">>> %s", (*term).c_str()); + + const auto stats{store->statistics()}; + g_assert_cmpuint(stats.size,==,store->size()); + g_assert_cmpuint(stats.last_index,==,0); + g_assert_cmpuint(stats.last_change,>=,::time({})); +} + + +static void +test_index_move() +{ + using namespace std::chrono_literals; + + const std::string msg_text = +R"(From: Valentine Michael Smith <mike@example.com> +To: Raul Endymion <raul@example.com> +Cc: emacs-devel@gnu.org +Subject: Re: multi-eq hash tables +Date: Tue, 03 May 2022 20:58:02 +0200 +Message-ID: <87h766tzzz.fsf@gnus.org> +MIME-Version: 1.0 +Content-Type: text/plain +Precedence: list +List-Id: "Emacs development discussions." <emacs-devel.gnu.org> +List-Post: <mailto:emacs-devel@gnu.org> + +Raul Endymion <raul@example.com> writes: + +> Maybe we should introduce something like: +> +> (define-hash-table-test shallow-equal +> (lambda (x1 x2) (while (and (consp x1) (consp x2) (eql (car x1) (car x2))) +> (setq x1 (cdr x1)) (setq x2 (cdr x2))) +> (equal x1 x2))) +> ...) + +Yes, that would be excellent. +)"; + + TempDir tempdir2; + + { // create a message file. + const auto res1 = maildir_mkdir(tempdir2.path() + "/Maildir/a"); + assert_valid_result(res1); + + std::ofstream output{tempdir2.path() + "/Maildir/a/new/msg"}; + output.write(msg_text.c_str(), msg_text.size()); + output.close(); + g_assert_true(output.good()); + } + + // Index it into a store. + TempDir tempdir; + auto store{Store::make_new(tempdir.path(), tempdir2.path() + "/Maildir", {}, {})}; + assert_valid_result(store); + + store->indexer().start({}); + size_t n{}; + while (store->indexer().is_running()) { + std::this_thread::sleep_for(100ms); + g_assert_cmpuint(n++,<=,25); + } + g_assert_true(!store->indexer().is_running()); + const auto& prog{store->indexer().progress()}; + g_assert_cmpuint(prog.updated,==,1); + g_assert_cmpuint(store->size(), ==, 1); + g_assert_false(store->empty()); + + // Find the message + auto qr = store->run_query("path:" + tempdir2.path() + "/Maildir/a/new/msg"); + assert_valid_result(qr); + g_assert_cmpuint(qr->size(),==,1); + + const auto msg = qr->begin().message(); + g_assert_true(!!msg); + + // Check the message + const auto oldpath{msg->path()}; + assert_equal(msg->subject(), "Re: multi-eq hash tables"); + g_assert_true(msg->docid() != 0); + g_debug("%s", msg->to_sexp().to_sexp_string().c_str()); + + // Move the message from new->cur + std::this_thread::sleep_for(1s); /* ctime should change */ + const auto msg3 = store->move_message(msg->docid(), {}, Flags::Seen); + assert_valid_result(msg3); + assert_equal(msg3->maildir(), "/a"); + assert_equal(msg3->path(), tempdir2.path() + "/Maildir/a/cur/msg:2,S"); + g_assert_true(::access(msg3->path().c_str(), R_OK)==0); + g_assert_false(::access(oldpath.c_str(), R_OK)==0); + + g_debug("%s", msg3->to_sexp().to_sexp_string().c_str()); + g_assert_cmpuint(store->size(), ==, 1); +} + + +static void +test_store_fail() +{ + { + const auto store = Store::make("/root/non-existent-path/12345"); + g_assert_false(!!store); + } + + { + const auto store = Store::make_new("/../../root/non-existent-path/12345", + "/../../root/non-existent-path/54321", + {}, {}); + g_assert_false(!!store); + } +} + + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_add_func("/store/ctor-dtor", test_store_ctor_dtor); + g_test_add_func("/store/add-count-remove", test_store_add_count_remove); + g_test_add_func("/store/message/mailing-list", + test_message_mailing_list); + g_test_add_func("/store/message/attachments", + test_message_attachments); + g_test_add_func("/store/index/move", test_index_move); + g_test_add_func("/store/index/fail", test_store_fail); + + return g_test_run(); +} diff --git a/lib/tests/test-parser.cc b/lib/tests/test-parser.cc new file mode 100644 index 0000000..74b5522 --- /dev/null +++ b/lib/tests/test-parser.cc @@ -0,0 +1,139 @@ +/* +** Copyright (C) 2017-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <vector> +#include <glib.h> + +#include <iostream> +#include <sstream> + +#include "utils/mu-test-utils.hh" + +#include "mu-parser.hh" +#include "utils/mu-result.hh" +#include "utils/mu-utils.hh" +using namespace Mu; + +struct Case { + const std::string expr; + const std::string expected; + WarningVec warnings{}; +}; + +using CaseVec = std::vector<Case>; + +static void +test_cases(const CaseVec& cases) +{ + char* tmpdir = test_mu_common_get_random_tmpdir(); + g_assert(tmpdir); + auto dummy_store{Store::make_new(tmpdir, "/tmp", {}, {})}; + assert_valid_result(dummy_store); + + g_free(tmpdir); + + Parser parser{*dummy_store, Parser::Flags::UnitTest}; + + for (const auto& casus : cases) { + WarningVec warnings; + const auto tree = parser.parse(casus.expr, warnings); + + std::stringstream ss; + ss << tree; + + if (g_test_verbose()) { + std::cout << "\n"; + std::cout << casus.expr << std::endl; + std::cout << "exp:" << casus.expected << std::endl; + std::cout << "got:" << ss.str() << std::endl; + } + + assert_equal(casus.expected, ss.str()); + } +} + +static void +test_basic() +{ + CaseVec cases = { + //{ "", R"#((atom :value ""))#"}, + { + "foo", + R"#((value "message-id" "foo"))#", + }, + {"foo or bar", R"#((or(value "message-id" "foo")(value "message-id" "bar")))#"}, + {"foo and bar", R"#((and(value "message-id" "foo")(value "message-id" "bar")))#"}, + }; + + test_cases(cases); +} + +static void +test_complex() +{ + CaseVec cases = { + {"foo and bar or cuux", + R"#((or(and(value "message-id" "foo")(value "message-id" "bar")))#" + + std::string(R"#((value "message-id" "cuux")))#")}, + {"a and not b", R"#((and(value "message-id" "a")(not(value "message-id" "b"))))#"}, + {"a and b and c", + R"#((and(value "message-id" "a")(and(value "message-id" "b")(value "message-id" "c"))))#"}, + {"(a or b) and c", + R"#((and(or(value "message-id" "a")(value "message-id" "b"))(value "message-id" "c")))#"}, + {"a b", // implicit and + R"#((and(value "message-id" "a")(value "message-id" "b")))#"}, + {"a not b", // implicit and not + R"#((and(value "message-id" "a")(not(value "message-id" "b"))))#"}, + {"not b", // implicit and not + R"#((not(value "message-id" "b")))#"}}; + + test_cases(cases); +} + +G_GNUC_UNUSED static void +test_range() +{ + CaseVec cases = { + {"range:a..b", // implicit and + R"#((range "range" "a" "b"))#"}, + }; + + test_cases(cases); +} + +static void +test_flatten() +{ + CaseVec cases = {{" Mötørhęåđ", R"#((value "message-id" "motorhead"))#"}}; + + test_cases(cases); +} + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/parser/basic", test_basic); + g_test_add_func("/parser/complex", test_complex); + // g_test_add_func ("/parser/range", test_range); + g_test_add_func("/parser/flatten", test_flatten); + + return g_test_run(); +} diff --git a/lib/tests/test-query.cc b/lib/tests/test-query.cc new file mode 100644 index 0000000..d1ca0bb --- /dev/null +++ b/lib/tests/test-query.cc @@ -0,0 +1,102 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include <config.h> + +#include <vector> +#include <glib.h> + +#include <iostream> +#include <sstream> +#include <unistd.h> + +#include "mu-store.hh" +#include "mu-query.hh" +#include "index/mu-indexer.hh" +#include "utils/mu-result.hh" +#include "utils/mu-utils.hh" +#include "utils/mu-test-utils.hh" + +using namespace Mu; + +static void +test_query() +{ + allow_warnings(); + char* tdir; + + tdir = test_mu_common_get_random_tmpdir(); + auto store = Store::make_new(tdir, std::string{MU_TESTMAILDIR}, {}, {}); + assert_valid_result(store); + g_free(tdir); + + auto&& idx{store->indexer()}; + + g_assert_true(idx.start(Indexer::Config{})); + while (idx.is_running()) { + sleep(1); + } + + auto dump_matches = [](const QueryResults& res) { + size_t n{}; + for (auto&& item : res) { + std::cout << item.query_match() << '\n'; + if (g_test_verbose()) + g_debug("%02zu %s %s", + ++n, + item.path().value_or("<none>").c_str(), + item.message_id().value_or("<none>").c_str()); + } + }; + + g_assert_cmpuint(store->size(), ==, 19); + + { + const auto res = store->run_query("", {}, QueryFlags::None); + g_assert_true(!!res); + g_assert_cmpuint(res->size(), ==, 19); + dump_matches(*res); + + g_assert_cmpuint(store->count_query(""), ==, 19); + + } + + { + const auto res = store->run_query("", Field::Id::Path, QueryFlags::None, 11); + g_assert_true(!!res); + g_assert_cmpuint(res->size(), ==, 11); + dump_matches(*res); + } +} + +int +main(int argc, char* argv[]) +try { + mu_test_init(&argc, &argv); + + g_test_add_func("/query", test_query); + + return g_test_run(); + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} catch (...) { + std::cerr << "caught exception\n"; + return 1; +} diff --git a/lib/tests/test-tokenizer.cc b/lib/tests/test-tokenizer.cc new file mode 100644 index 0000000..6e287f0 --- /dev/null +++ b/lib/tests/test-tokenizer.cc @@ -0,0 +1,147 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <vector> +#include <glib.h> +#include <iostream> +#include <sstream> + +#include "mu-tokenizer.hh" + +struct Case { + const char* str; + const Mu::Tokens tokens; +}; + +using CaseVec = std::vector<Case>; + +using namespace Mu; +using TT = Token::Type; + +static void +test_cases(const CaseVec& cases) +{ + for (const auto& casus : cases) { + const auto tokens = tokenize(casus.str); + + g_assert_cmpuint((guint)tokens.size(), ==, (guint)casus.tokens.size()); + for (size_t u = 0; u != tokens.size(); ++u) { + if (g_test_verbose()) { + std::cerr << "case " << u << " " << casus.str << std::endl; + std::cerr << "exp: '" << casus.tokens[u] << "'" << std::endl; + std::cerr << "got: '" << tokens[u] << "'" << std::endl; + } + g_assert_true(tokens[u] == casus.tokens[u]); + } + } +} + +static void +test_basic() +{ + CaseVec cases = { + {"", {}}, + + {"foo", Tokens{Token{3, TT::Data, "foo"}}}, + + {"foo bar cuux", + Tokens{Token{3, TT::Data, "foo"}, + Token{7, TT::Data, "bar"}, + Token{12, TT::Data, "cuux"}}}, + + {"\"foo bar\"", Tokens{Token{9, TT::Data, "foo bar"}}}, + + // ie. ignore missing closing '"' + {"\"foo bar", Tokens{Token{8, TT::Data, "foo bar"}}}, + + }; + + test_cases(cases); +} + +static void +test_specials() +{ + CaseVec cases = { + {")*(", + Tokens{Token{1, TT::Close, ")"}, Token{2, TT::Data, "*"}, Token{3, TT::Open, "("}}}, + {"\")*(\"", Tokens{Token{5, TT::Data, ")*("}}}, + }; + + test_cases(cases); +} + +static void +test_ops() +{ + CaseVec cases = {{"foo and bar oR cuux XoR fnorb", + Tokens{Token{3, TT::Data, "foo"}, + Token{7, TT::And, "and"}, + Token{11, TT::Data, "bar"}, + Token{14, TT::Or, "oR"}, + Token{19, TT::Data, "cuux"}, + Token{23, TT::Xor, "XoR"}, + Token{29, TT::Data, "fnorb"}}}, + {"NOT (aap or mies)", + Tokens{Token{3, TT::Not, "NOT"}, + Token{5, TT::Open, "("}, + Token{8, TT::Data, "aap"}, + Token{11, TT::Or, "or"}, + Token{16, TT::Data, "mies"}, + Token{17, TT::Close, ")"}}}}; + + test_cases(cases); +} + +static void +test_escape() +{ + CaseVec cases = {{"foo\"bar\"", Tokens{Token{8, TT::Data, "foobar"}}}, + {"\"fnorb\"", Tokens{Token{7, TT::Data, "fnorb"}}}, + {"\\\"fnorb\\\"", Tokens{Token{9, TT::Data, "fnorb"}}}, + {"foo\\\"bar\\\"", Tokens{Token{10, TT::Data, "foobar"}}}}; + + test_cases(cases); +} + +static void +test_to_string() +{ + std::stringstream ss; + for (auto&& t : tokenize("foo and bar xor not cuux or fnorb")) + ss << t << ' '; + + g_assert_true(ss.str() == "3: <data> [foo] 7: <and> [and] 11: <data> [bar] " + "15: <xor> [xor] 19: <not> [not] 24: <data> [cuux] " + "27: <or> [or] 33: <data> [fnorb] "); +} + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/tokens/basic", test_basic); + g_test_add_func("/tokens/specials", test_specials); + g_test_add_func("/tokens/ops", test_ops); + g_test_add_func("/tokens/escape", test_escape); + g_test_add_func("/tokens/to-string", test_to_string); + + return g_test_run(); +} diff --git a/lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S b/lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S new file mode 100644 index 0000000..ab1500f --- /dev/null +++ b/lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S @@ -0,0 +1,146 @@ +Return-Path: <gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX, + RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:19 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008 + 20:15:17 -0700 (PDT) +Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06 + Aug 2008 20:15:16 -0700 (PDT) +Received: from sourceware.org (sourceware.org [209.132.176.174]) by + mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug + 2008 20:15:16 -0700 (PDT) +Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor + denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + client-ip=209.132.176.174; +Authentication-Results: mx.google.com; spf=neutral (google.com: + 209.132.176.174 is neither permitted nor denied by domain of + gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org +Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000 +Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000 +Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7) + by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000 +Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by + mailgw1a.lmco.com (LM-6) with ESMTP id m773EPZH014730for + <gcc-help@gcc.gnu.org>; Wed, 6 Aug 2008 21:14:25 -0600 (MDT) +Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428) + id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 + 21:14:25 -0600 (MDT) +Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF + V6.3-x14 #31428) with ESMTP id <0K5700H5MNNWGX@lmco.com> for + gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT) +Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by + EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed, 06 Aug + 2008 23:14:20 -0400 +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Mickey Mouse" <anon@example.com> +Subject: gcc include search order +To: "Donald Duck" <gcc-help@gcc.gnu.org> +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Content-class: urn:content-classes:message +Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm +Precedence: klub +List-Id: <gcc-help.gcc.gnu.org> +List-Unsubscribe: <mailto:gcc-help-unsubscribe-xxxx.klub=gmail.com@gcc.gnu.org> +List-Archive: <http://gcc.gnu.org/ml/gcc-help/> +List-Post: <mailto:gcc-help@gcc.gnu.org> +List-Help: <mailto:gcc-help-help@gcc.gnu.org> +Sender: gcc-help-owner@gcc.gnu.org +Delivered-To: mailing list gcc-help@gcc.gnu.org +Content-Length: 3024 + + +Hi. +In my unit testing I need to change some header files (target is +vxWorks, which supports some things that the sun does not). +So, what I do is fetch the development tree, and then in a new unit test +directory I attempt to compile the unit under test. Since this is NOT +vxworks, I use sed to change some of the .h files and put them in a +./changed directory. + +When I try to compile the file, it is still using the .h file from the +original location, even though I have listed the include path for +./changed before the include path for the development tree. + +Here is a partial output from gcc using the -v option + +GNU CPP version 3.1 (cpplib) (sparc ELF) +GNU C++ version 3.1 (sparc-sun-solaris2.8) + compiled by GNU C version 3.1. +ignoring nonexistent directory "NONE/include" +#include "..." search starts here: +#include <...> search starts here: + . + changed + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface + /usr/local/include/g++-v3 + /usr/local/include/g++-v3/sparc-sun-solaris2.8 + /usr/local/include/g++-v3/backward + /usr/local/include + /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include + /usr/local/sparc-sun-solaris2.8/include + /usr/include +End of search list. + +I know the changed file is correct and that the include is not working +as expected, because when I copy the file from ./changed, back into the +development tree, the compilation works as expected. + +One more bit of information. The source that I cam compiling is in +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app +And it is including files from +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +These include files should be including the files from ./changed (when +they exist) but they are ignoring the .h files in the ./changed +directory and are instead using other, unchanged files in the +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +directory. + +The gcc command line is something like + + TEST_DIR="." + + CHANGED_DIR_NAME=changed + CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME} + + CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I +${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}" + + HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}" + DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST" + + CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow +-Wmissing-prototypes -Wmissing-declarations" + + printf "Compiling the UUT File\n" + gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES} +${AP_APP_FILES}/unitUnderTest.cpp + + +I hope this explanation is clear. If anyone knows how to fix the command +line so that it gets the .h files in the "changed" directory are used +instead of files in the other include directories. + +Thanks +Joe + +---------------------------------------------------- +Time Flies like an Arrow. Fruit Flies like a Banana + + diff --git a/lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S b/lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S new file mode 100644 index 0000000..d0ff0d7 --- /dev/null +++ b/lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S @@ -0,0 +1,230 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00,HTML_MESSAGE + autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id D724F6963B + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:27 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:27 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs86537wfy; Mon, 4 Aug 2008 00:38:51 + -0700 (PDT) +Received: by 10.151.113.5 with SMTP id q5mr272266ybm.37.1217835529913; Mon, 04 + Aug 2008 00:38:49 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 5si5754915ywd.8.2008.08.04.00.38.30; Mon, 04 Aug 2008 00:38:50 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id 765A511C46; Mon, 4 Aug 2008 03:38:27 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from ik-out-1112.google.com (ik-out-1112.google.com [66.249.90.176]) + by sqlite.org (Postfix) with ESMTP id 4C59511C41 for <sqlite-dev@sqlite.org>; + Mon, 4 Aug 2008 03:38:23 -0400 (EDT) +Received: by ik-out-1112.google.com with SMTP id b32so2163423ika.0 for + <sqlite-dev@sqlite.org>; Mon, 04 Aug 2008 00:38:23 -0700 (PDT) +Received: by 10.210.54.19 with SMTP id c19mr14589042eba.107.1217835502549; + Mon, 04 Aug 2008 00:38:22 -0700 (PDT) +Received: by 10.210.115.10 with HTTP; Mon, 4 Aug 2008 00:38:22 -0700 (PDT) +Message-ID: <477821040808040038s381bf382p7411451e3c1a2e4e@mail.gmail.com> +Date: Mon, 4 Aug 2008 10:38:22 +0300 +From: anon@example.com +To: sqlite-dev@sqlite.org +In-Reply-To: <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com> +MIME-Version: 1.0 +References: <477821040808030533y41f1501dq32447b568b6e6ca5@mail.gmail.com> + <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com> +Subject: Re: [sqlite-dev] SQLite exception A&B +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Priority: normal +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Content-Type: multipart/mixed; boundary="===============2123623832==" +Mime-version: 1.0 +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 8475 + +--===============2123623832== +Content-Type: multipart/alternative; + boundary="----=_Part_29556_25702991.1217835502493" + +------=_Part_29556_25702991.1217835502493 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Hi Grant, + +Thanks for your reply. +I am using a different session for each thread, whenever a thread wishes to +access the database it gets a session from the session pool and works with +that session until its work is done. + +Most of the actions the threads are doing on the database are quite +complicated and are required to be fully committed or completely ignored, so +yes, I am (most of the time) explicitly beginning and committing my +transactions. + +Regarding the SQLiteStatementImpl, I believe the Poco manual explains that +sessions and statements for that matter cannot be shared between threads, +therefore if you are using a session via one thread only it should work +fine. + +My first impression was that the problem was in the Poco infrastructure (I +have found several Poco related bugs in the past), but the problem ALWAYS +occurs when I perform the "BEGIN IMMEDIATE" action, if it were a Poco +related bug, I would expect to see it here and there without any relation to +this specific statement, but that is not the case. + +None the less, I will also post my question on the Poco forums. + +Nadav. + +On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <grant.gatchel@gmail.com>wrote: + +> Are you using the same Poco::Session for every thread or does each call +> create a new session/handle to the database? +> +> Are you explicitly BEGINning and COMMITting your transactions? +> +> In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a +> race condition in the SQLiteStatementImpl::next() method in which the member +> _nextResponse is being accessed before the SQLiteStatementImpl::hasNext() +> method has a chance to interpret that value and throw an exception. +> +> This question might be more suitable in the Poco forums or mailinglist. +> +> - Grant +> +> On Sun, Aug 3, 2008 at 8:33 AM, nadav g <nadav.gr@gmail.com> wrote: +> +>> Hi All, +>> +>> I have been using SQLite with Poco (www.appinf.com) as my infrastructure. +>> The program is running several threads that access this database very +>> often and are synchronized by SQLite itself. +>> Everything seems to work just fine most of time (usually days - weeks) but +>> I do get an occasional exception: +>> +>> Exception: SQL error or missing database: Iterator Error: trying to check +>> if there is a next value +>> +>> The backtrace leads to this statement: +>> *"BEGIN IMMEDIATE"* +>> +>> This specific code runs numerous times before an exception occurs (if +>> occurs at all) and I cannot think of any reason for it to fail later rather +>> than sooner. +>> It is pretty obvious that this situation occurs due to some rare thread +>> state, but I could not find any information that gives me any hint as to +>> what this state might be. +>> +>> So what I am asking is: +>> 1) Does anyone know why this sort of exception occurs? +>> 2) Can anyone think of a reason for such an exception to occur in the +>> situation I have described? +>> +>> Thanks in advance, +>> Nadav. +>> +>> +>> _______________________________________________ +>> sqlite-dev mailing list +>> sqlite-dev@sqlite.org +>> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev +>> +>> +> +> _______________________________________________ +> sqlite-dev mailing list +> sqlite-dev@sqlite.org +> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev +> +> + +------=_Part_29556_25702991.1217835502493 +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +<div dir="ltr">Hi Grant,<br><br>Thanks for your reply.<br>I am using a different session for each thread, whenever a thread wishes to access the database it gets a session from the session pool and works with that session until its work is done.<br> +<br>Most of the actions the threads are doing on the database are quite complicated and are required to be fully committed or completely ignored, so yes, I am (most of the time) explicitly beginning and committing my transactions.<br> +<br>Regarding the SQLiteStatementImpl, I believe the Poco manual explains that sessions and statements for that matter cannot be shared between threads, therefore if you are using a session via one thread only it should work fine.<br> +<br>My first impression was that the problem was in the Poco infrastructure (I have found several Poco related bugs in the past), but the problem ALWAYS occurs when I perform the "BEGIN IMMEDIATE" action, if it were a Poco related bug, I would expect to see it here and there without any relation to this specific statement, but that is not the case.<br> +<br>None the less, I will also post my question on the Poco forums.<br><br>Nadav.<br><br><div class="gmail_quote">On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <span dir="ltr"><<a href="mailto:grant.gatchel@gmail.com">grant.gatchel@gmail.com</a>></span> wrote:<br> +<blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;"><div dir="ltr">Are you using the same Poco::Session for every thread or does each call create a new session/handle to the database?<br> +<br>Are you explicitly BEGINning and COMMITting your transactions?<br><br>In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a race condition in the SQLiteStatementImpl::next() method in which the member _nextResponse is being accessed before the SQLiteStatementImpl::hasNext() method has a chance to interpret that value and throw an exception.<br> + +<br>This question might be more suitable in the Poco forums or mailinglist.<br><br>- Grant<br> +<br><div class="gmail_quote"><div><div></div><div class="Wj3C7c"> +On Sun, Aug 3, 2008 at 8:33 AM, nadav g <span dir="ltr"><<a href="http://nadav.gr" target="_blank">nadav.gr</a>@<a href="http://gmail.com" target="_blank">gmail.com</a>></span> wrote:<br></div></div><blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;"> +<div><div></div><div class="Wj3C7c"> + + +<div dir="ltr">Hi All,<br><br>I have been using SQLite with Poco (<a href="http://www.appinf.com" target="_blank">www.appinf.com</a>) as my infrastructure.<br>The program is running several threads that access this database very often and are synchronized by SQLite itself.<br> + + + + +Everything seems to work just fine most of time (usually days - weeks) but I do get an occasional exception:<br><br>Exception: SQL error or missing database: Iterator Error: trying to check if there is a next value<br><br> + + + + +The backtrace leads to this statement:<br><b>"BEGIN IMMEDIATE"</b><br><br>This specific code runs numerous times before an exception occurs (if occurs at all) and I cannot think of any reason for it to fail later rather than sooner.<br> + + + + +It is pretty obvious that this situation occurs due to some rare thread state, but I could not find any information that gives me any hint as to what this state might be.<br><br>So what I am asking is:<br>1) Does anyone know why this sort of exception occurs?<br> + + + + +2) Can anyone think of a reason for such an exception to occur in the situation I have described?<br><br>Thanks in advance,<br>Nadav.<br><br></div> +<br></div></div>_______________________________________________<br> +sqlite-dev mailing list<br> +<a href="mailto:sqlite-dev@sqlite.org" target="_blank">sqlite-dev@sqlite.org</a><br> +<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br> +<br></blockquote></div><br></div> +<br>_______________________________________________<br> +sqlite-dev mailing list<br> +<a href="mailto:sqlite-dev@sqlite.org">sqlite-dev@sqlite.org</a><br> +<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br> +<br></blockquote></div><br></div> + +------=_Part_29556_25702991.1217835502493-- + +--===============2123623832== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + +--===============2123623832==-- + diff --git a/lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS b/lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS new file mode 100644 index 0000000..d6487c0 --- /dev/null +++ b/lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS @@ -0,0 +1,136 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW, + SPF_PASS,WHOIS_NETSOLPR autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 1A6CD69CB6 + for <xxxx@localhost>; Tue, 12 Aug 2008 21:42:38 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Tue, 12 Aug 2008 21:42:38 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs123119wfh; Sun, 10 Aug 2008 + 22:06:31 -0700 (PDT) +Received: by 10.100.166.10 with SMTP id o10mr9327844ane.0.1218431190107; Sun, + 10 Aug 2008 22:06:30 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id c29si10110392anc.13.2008.08.10.22.06.29; Sun, 10 Aug 2008 + 22:06:30 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:45637 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KSPbx-0006dj-96 for + xxxx.klub@gmail.com; Mon, 11 Aug 2008 01:06:29 -0400 +Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id + 1KSPbE-0006cQ-Nd for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400 +Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id + 1KSPbD-0006bs-Px for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400 +Received: from [199.232.76.173] (port=37426 helo=monty-python.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KSPbD-0006bk-HT for + help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400 +Received: from main.gmane.org ([80.91.229.2]:46446 helo=ciao.gmane.org) by + monty-python.gnu.org with esmtps (TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim + 4.60) (envelope-from <geh-help-gnu-emacs@m.gmane.org>) id 1KSPbD-0003Kl-CA + for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400 +Received: from list by ciao.gmane.org with local (Exim 4.43) id + 1KSPb9-00080r-CX for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 05:05:39 +0000 +Received: from bas2-toronto63-1088792724.dsl.bell.ca ([64.229.168.148]) by + main.gmane.org with esmtp (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for + <help-gnu-emacs@gnu.org>; Mon, 11 Aug 2008 05:05:39 +0000 +Received: from cpchan by bas2-toronto63-1088792724.dsl.bell.ca with local + (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for <help-gnu-emacs@gnu.org>; Mon, + 11 Aug 2008 05:05:39 +0000 +X-Injected-Via-Gmane: http://gmane.org/ +To: help-gnu-emacs@gnu.org +From: anon@example.com +Date: Mon, 11 Aug 2008 01:03:22 -0400 +Organization: Linux Private Site +Message-ID: <87bq00nnxh.fsf@MagnumOpus.Mercurius> +References: <877iav5s49.fsf@163.com> <86hc9yc5sj.fsf@timbral.net> + <877iat7udd.fsf@163.com> <87fxphcsxi.fsf@lion.rapttech.com.au> + <8504ddd4-5e3b-4ed5-bf77-aa9cce81b59a@1g2000pre.googlegroups.com> + <87k5es59we.fsf@lion.rapttech.com.au> + <63c824e3-62b1-4a93-8fa8-2813e1f9397f@v13g2000pro.googlegroups.com> + <874p5vsgg8.fsf@nonospaz.fatphil.org> + <8250972e-1886-4021-80bc-376e34881c80@v39g2000pro.googlegroups.com> + <87zlnnqvvs.fsf@nonospaz.fatphil.org> + <57add0e0-b39d-4c71-8d2c-d3b9ddfaa1a9@1g2000pre.googlegroups.com> + <87sktfnz5p.fsf@atthis.clsnet.nl> + <562e1111-d9e7-4b6a-b661-3f9af13fea17@b30g2000prf.googlegroups.com> + <87d4khoq97.fsf@atthis.clsnet.nl> + <0fe404c5-cab8-4692-8a27-532e737a7813@i24g2000prf.googlegroups.com> +Mime-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1; + protocol="application/pgp-signature" +X-Complaints-To: usenet@ger.gmane.org +X-Gmane-NNTP-Posting-Host: bas2-toronto63-1088792724.dsl.bell.ca +X-Face: G; + Z,`sm>)4t4LB/GUrgH$W`!AmfHMj,LG)Z}X0ax@s9:0>0)B&@vcm{v-le)wng)?|o]D<V6&ay<F=H{M5?$T%p!dPdJeF,au\E@TA"v22K!Zl\\mzpU4]6$ZnAI3_L)h; + fpd}mn2py/7gv^|*85-D_f:07cT>\Z}0:6X +User-Agent: Gnus/5.110011 (No Gnus v0.11) Emacs/23.0.60 (gnu/linux) +Cancel-Lock: sha1:IKyfrl5drOw6HllHFSmWHAKEeC8= +X-detected-kernel: by monty-python.gnu.org: Linux 2.6, seldom 2.4 (older, 4) +Subject: Re: Can anybody tell me how to send HTML-format mail in gnus +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 1229 +Lines: 36 + +--=-=-= +Content-Type: text/plain + +Xah <xahlee@gmail.com> writes: + +> So, i was reading about it in Wikipedia. Although i don't have a TV, +> and haven't had since 2000, but i still enjoyed the festive spirits +> anyhow. After all, i'm Chinese by blood. So, in my wandering, i ran +> into this welcome song on youtube: +> +> http://www.youtube.com/watch?v=1HEndNYVhZo + +What is your point? Your email is in plain text and I can click on the +link just fine- it is not exactly rocket science to implement parsing of +URL's to workable links in an Email program (a lot of programs does +that, including Gnus). Images can be included inline if you want. Also +mail markups such as *this*, **this** and _this_ have been around since +the Usenet days and displayed appropriately by a number of mailers. Like +others have said, most html messages that I have seen either contains +useless information, or are plain spam and can introduce a host of +security problems in some mailers. + +Charles + + +--=-=-= +Content-Type: application/pgp-signature + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v2.0.4-svn0 (GNU/Linux) + +iD8DBQFIn8gm3epPyyKbwPYRApbvAKDRirXwzMzI+NHV77+QcP3EgTPaCgCfb/6m +GtNVKdYAeftaYm1nwRVoCDA= +=ULo3 +-----END PGP SIGNATURE----- +--=-=-=-- + + + diff --git a/lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S b/lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S new file mode 100644 index 0000000..78efa2a --- /dev/null +++ b/lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S @@ -0,0 +1,77 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:08 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008 + 13:40:29 -0700 (PDT) +Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed, + 06 Aug 2008 13:40:28 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008 + 13:40:28 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400 +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Wed, 6 Aug 2008 20:38:35 +0100 +Message-ID: <r6bpm5-6n6.ln1@news.ducksburg.com> +References: <55dbm5-qcl.ln1@news.ducksburg.com> + <mailman.15710.1217599959.18990.help-gnu-emacs@gnu.org> +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv +X-Orig-Path: news.ducksburg.com!news +Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94= + sha1:oepBoM0tJBLN52DotWmBBvW5wbg= +User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy) +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail +Xref: news.stanford.edu gnu.emacs.help:160868 +To: help-gnu-emacs@gnu.org +Subject: Re: Learning LISP; Scheme vs elisp. +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 417 +Lines: 11 + +On 2008-08-01, Thien-Thi Nguyen wrote: + +> warriors attack, felling foe after foe, +> few growing old til they realize: to know +> what deceit is worth deflection; +> such receipt reversed rejection! +> then their heavy arms, e'er transformed to shields: +> balanced hooked charms, ploughed deep, rich yields. + +Aha: the exercise for the reader is to place the parens correctly. +Might take me a while to solve this puzzle. + diff --git a/lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S b/lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S new file mode 100644 index 0000000..de46cc8 --- /dev/null +++ b/lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S @@ -0,0 +1,84 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:34 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs89397wfy; Mon, 4 Aug 2008 02:41:16 + -0700 (PDT) +Received: by 10.150.156.20 with SMTP id d20mr963580ybe.104.1217842875596; Mon, + 04 Aug 2008 02:41:15 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 6si3605185ywi.1.2008.08.04.02.40.57; Mon, 04 Aug 2008 02:41:15 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id 7147F11C45; Mon, 4 Aug 2008 05:40:55 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from relay00.pair.com (relay00.pair.com [209.68.5.9]) by sqlite.org + (Postfix) with SMTP id B5F901192C for <sqlite-dev@sqlite.org>; Mon, 4 Aug + 2008 05:40:52 -0400 (EDT) +Received: (qmail 59961 invoked from network); 4 Aug 2008 09:40:50 -0000 +Received: from unknown (HELO ?192.168.0.17?) (unknown) by unknown with SMTP; 4 + Aug 2008 09:40:50 -0000 +X-pair-Authenticated: 87.13.75.164 +Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: anon@example.com +To: sqlite-dev@sqlite.org +Mime-Version: 1.0 (Apple Message framework v926) +Date: Mon, 4 Aug 2008 11:40:49 +0200 +X-Mailer: Apple Mail (2.926) +Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 639 + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; +--- +Marco Bambini +http://www.sqlabs.net +http://www.sqlabs.net/blog/ +http://www.sqlabs.net/realsqlserver/ + + + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + diff --git a/lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS b/lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS new file mode 100644 index 0000000..b5c0651 --- /dev/null +++ b/lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS @@ -0,0 +1,138 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 3EBAB6963B + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:35 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:35 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs89536wfy; Mon, 4 Aug 2008 02:48:56 + -0700 (PDT) +Received: by 10.150.134.21 with SMTP id h21mr7950048ybd.181.1217843335665; + Mon, 04 Aug 2008 02:48:55 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 6si5897081ywi.1.2008.08.04.02.48.35; Mon, 04 Aug 2008 02:48:55 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id ED01611C4E; Mon, 4 Aug 2008 05:48:31 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from mx0.security.ro (mx0.security.ro [80.96.72.194]) by sqlite.org + (Postfix) with ESMTP id EB3F51192C for <sqlite-dev@sqlite.org>; Mon, 4 Aug + 2008 05:48:28 -0400 (EDT) +Received: (qmail 348 invoked from network); 4 Aug 2008 12:48:03 +0300 +Received: from dev.security.ro (HELO ?192.168.1.70?) (192.168.1.70) by + mx0.security.ro with SMTP; 4 Aug 2008 12:48:03 +0300 +Message-ID: <4896D06A.8000901@security.ro> +Date: Mon, 04 Aug 2008 12:48:26 +0300 +From: anon@example.com +User-Agent: Thunderbird 2.0.0.16 (Windows/20080708) +MIME-Version: 1.0 +To: sqlite-dev@sqlite.org +References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +In-Reply-To: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +Content-Type: multipart/mixed; boundary="------------000207070200050102060301" +X-BitDefender-Scanner: Clean, Agent: BitDefender qmail 2.0.0 on + mx0.security.ro +X-BitDefender-Spam: No (0) +X-BitDefender-SpamStamp: v1, whitelisted, total: 0 +Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Precedence: high +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 2212 + +This is a multi-part message in MIME format. +--------------000207070200050102060301 +Content-Type: text/plain; charset=ISO-8859-1; format=flowed +Content-Transfer-Encoding: 7bit + +Marco Bambini wrote: +> Inside sqlite3VdbeExec there is a very big switch statement. +> In order to increase performance with few modifications to the +> original code, why not use this technique ? +> http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html +> +> With a properly defined "instructions" array, instead of the switch +> statement you can use something like: +> goto * instructions[pOp->opcode]; +> --- +> Marco Bambini +> http://www.sqlabs.net +> http://www.sqlabs.net/blog/ +> http://www.sqlabs.net/realsqlserver/ +> +> +> +> _______________________________________________ +> sqlite-dev mailing list +> sqlite-dev@sqlite.org +> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev +> +All the world's not a VAX. This technique is GCC-specific. The SQLite +source must be as portable as possible thus tying it to a specific +compiler is out of the question. While one could conceivably use some +preprocessor magic to provide alternate implementations, that would be +impractical considering the sheer size of the code affected. +On the other hand - perhaps you could benchmark the change and provide +some data on whether this actually improves performance? + + +--------------000207070200050102060301 +Content-Type: text/x-vcard; charset=utf-8; + name="mihailim.vcf" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="mihailim.vcf" + +begin:vcard +fn:Mihai Limbasan +n:Limbasan;Mihai +org:SC SECPRAL COM SRL +adr:;;str. Actorului nr. 9;Cluj-Napoca;Cluj;400441;Romania +email;internet:mihailim@security.ro +title:SoftwareDeveloper +tel;work:+40 264 449579 +tel;fax:+40 264 418594 +tel;cell:+40 729 038302 +url:http://secpral.ro/ +version:2.1 +end:vcard + + +--------------000207070200050102060301 +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + +--------------000207070200050102060301-- + diff --git a/lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S b/lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S new file mode 100644 index 0000000..4fad706 --- /dev/null +++ b/lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S @@ -0,0 +1,21 @@ +Return-Path: <dfgh@floppydisk.nl> +X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime +X-Spam-Level: +Delivered-To: dfgh@floppydisk.nl +Message-ID: <43A09C49.9040902@euler.org> +Date: Wed, 14 Dec 2005 23:27:21 +0100 +From: Fred Flintstone <fred@euler.org> +User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010) +X-Accept-Language: nl-NL, nl, en +MIME-Version: 1.0 +To: dfgh@floppydisk.nl +Subject: Re: xyz +References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org> +In-Reply-To: <20051211184308.GB13513@gauss.org> +X-Enigmail-Version: 0.92.0.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-UIDL: T<?"!%LG"!cAK"!_j(#! +Content-Length: 1879 + +Test 123. diff --git a/lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2, b/lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2, new file mode 100644 index 0000000..25c7180 --- /dev/null +++ b/lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2, @@ -0,0 +1,16 @@ +From: Frodo Baggins <frodo@example.com> +To: Bilbo Baggins <bilbo@anotherexample.com> +Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +Fcc: .sent +Organization: The Fellowship of the Ring +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Message-Id: <abcd$efgh@example.com> + + +Let's write some fünkÿ text +using umlauts. + +Foo. diff --git a/lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2, b/lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2, new file mode 100644 index 0000000..863f714 --- /dev/null +++ b/lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2, @@ -0,0 +1,17 @@ +From: =?UTF-8?B?TcO8?= <testmu@testmu.xx> +To: Helmut =?UTF-8?B?S3LDtmdlcg==?= <hk@testmu.xxx> +Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +References: <non-exist-01@msg.id> <non-exist-02@msg.id> <non-exist-03@msg.id> <non-exist-04@msg.id> +1n-Reply-To: <non-exist-04@msg.id> +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + + +Test for issue #38, where apparently searching for accented words in subject, +to etc. fails. + +What about here? Queensrÿche. Mötley Crüe. + + diff --git a/lib/tests/testdir/cur/encrypted!2,S b/lib/tests/testdir/cur/encrypted!2,S new file mode 100644 index 0000000..f75fd40 --- /dev/null +++ b/lib/tests/testdir/cur/encrypted!2,S @@ -0,0 +1,56 @@ +Return-path: <> +Envelope-to: peter@example.com +Delivery-date: Fri, 11 May 2012 16:22:03 +0300 +Received: from localhost.example.com ([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-Ux + for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300 +Delivered-To: peter@example.com +From: Brian <brian@example.com> +To: Peter <peter@example.com> +Subject: encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:21:42 +0300 +Message-ID: <!&!AAAAAAAAAYAAAAAAAAAOH1+8mkk+lLn7Gg5fke7FbCgAAAEAAAAJ7eBDgcactKhXL6r8cEnJ8BAAAAAA==@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav +gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d +Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/ +gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v +cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4 +mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy +O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz +gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR +mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym +Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc +4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP +/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l +KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy +gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf +QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref +Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d +gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9 +we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM +qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU +9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY +HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM +0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG +2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ +IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH +=0Paa +-----END PGP MESSAGE----- +--=-=-=-- diff --git a/lib/tests/testdir/cur/multimime!2,FS b/lib/tests/testdir/cur/multimime!2,FS new file mode 100644 index 0000000..84f85aa --- /dev/null +++ b/lib/tests/testdir/cur/multimime!2,FS @@ -0,0 +1,27 @@ +Return-path: <> +Envelope-to: djcb@localhost +Delivery-date: Sun, 20 May 2012 09:59:51 +0300 +From: Steve Jobs <jobs@example.com> +To: Bill Gates <bg@example.com> +Subject: multimime +User-agent: mu4e 0.9.8.4; emacs 23.3.1 +Date: Sat, 19 May 2012 20:57:56 +0100 +Message-ID: <m2fwaw2baz.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain + +abc +--=-=-= +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="test1.C" +Content-Transfer-Encoding: base64 + +aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg== +--=-=-= +Content-Type: text/plain + +def +--=-=-=-- diff --git a/lib/tests/testdir/cur/multirecip!2,S b/lib/tests/testdir/cur/multirecip!2,S new file mode 100644 index 0000000..c997503 --- /dev/null +++ b/lib/tests/testdir/cur/multirecip!2,S @@ -0,0 +1,11 @@ +Date: Thu, 15 May 2016 14:57:25 -0200 +From: +To: a@example.com,b@example.com,c@example.com +Cc: d@example.com,e@example.com +Subject: test with multi to and cc +Message-id: <3BE9E652343245@emss35m06.us.lmco.com> + +Message with multi cc and to. + + + diff --git a/lib/tests/testdir/cur/signed!2,S b/lib/tests/testdir/cur/signed!2,S new file mode 100644 index 0000000..a2e7e21 --- /dev/null +++ b/lib/tests/testdir/cur/signed!2,S @@ -0,0 +1,36 @@ +Return-path: <> +Envelope-to: skipio@localhost +Delivery-date: Fri, 11 May 2012 16:21:57 +0300 +Received: from localhost.roma.net([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-55 + for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300 +Delivered-To: diggler@gmail.com +From: Skipio <skipio@roma.net> +To: Hannibal <hanni@carthago.net> +Subject: signed +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:20:45 +0300 +Message-ID: <878vgy97ma.fsf@roma.net> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1; + protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain + + +I am signed! + +--=-=-= +Content-Type: application/pgp-signature + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p +DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs +=blXz +-----END PGP SIGNATURE----- +--=-=-=-- + diff --git a/lib/tests/testdir/cur/signed-encrypted!2,S b/lib/tests/testdir/cur/signed-encrypted!2,S new file mode 100644 index 0000000..a3910e6 --- /dev/null +++ b/lib/tests/testdir/cur/signed-encrypted!2,S @@ -0,0 +1,54 @@ +Return-path: <> +Envelope-to: karjala@localhost +Delivery-date: Fri, 11 May 2012 16:37:57 +0300 +From: karjala@example.com +To: lapinkulta@example.com +Subject: signed + encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:36:08 +0300 +Message-ID: <874nrm96wn.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16 +uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb +L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J +P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj +cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg +zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW +C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e +YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ +QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA +zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ +GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ +AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v +bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx +ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg +cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM +DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR +w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx +lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs +YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni +Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ +IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI +JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB +t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY +6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr +kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I +C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA= +=pv03 +-----END PGP MESSAGE----- +--=-=-=-- + diff --git a/lib/tests/testdir/cur/special!2,Sabc b/lib/tests/testdir/cur/special!2,Sabc new file mode 100644 index 0000000..7f1de8e --- /dev/null +++ b/lib/tests/testdir/cur/special!2,Sabc @@ -0,0 +1,10 @@ +Date: Thu, 1 Jun 2012 14:57:25 -0200 +From: "Rocky Balboa" <rocky@example.com> +To: "Ivan Drago" <ivan@example.com> +Subject: currying and tail optimization +Message-id: <3BE9E653ef345@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT + +Test 123. I'm a special message with special flags. diff --git a/lib/tests/testdir/new/1220863087.12663_21.mindcrime b/lib/tests/testdir/new/1220863087.12663_21.mindcrime new file mode 100644 index 0000000..4101716 --- /dev/null +++ b/lib/tests/testdir/new/1220863087.12663_21.mindcrime @@ -0,0 +1,111 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 6389969CB2 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:07 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:07 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs34769wfh; Wed, 6 Aug 2008 + 13:38:53 -0700 (PDT) +Received: by 10.100.6.13 with SMTP id 13mr4103508anf.83.1218055131215; Wed, 06 + Aug 2008 13:38:51 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id b32si10199298ana.34.2008.08.06.13.38.49; Wed, 06 Aug 2008 + 13:38:51 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +DomainKey-Status: good (test mode) +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; domainkeys=pass + (test mode) header.From=juanma_bellon@yahoo.es +Received: from localhost ([127.0.0.1]:55648 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQpmT-0005W9-AQ for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:38:49 -0400 +Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id + 1KQplz-0005U5-Pk for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400 +Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id + 1KQplw-0005Nw-OG for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400 +Received: from [199.232.76.173] (port=45465 helo=monty-python.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQplw-0005NX-I6 for + help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:16 -0400 +Received: from n74a.bullet.mail.sp1.yahoo.com ([98.136.45.21]:29868) by + monty-python.gnu.org with smtp (Exim 4.60) (envelope-from + <juanma_bellon@yahoo.es>) id 1KQplw-0007EF-7Z for help-gnu-emacs@gnu.org; + Wed, 06 Aug 2008 16:38:16 -0400 +Received: from [216.252.122.216] by n74.bullet.mail.sp1.yahoo.com with NNFMP; + 06 Aug 2008 20:38:14 -0000 +Received: from [68.142.237.89] by t1.bullet.sp1.yahoo.com with NNFMP; 06 Aug + 2008 20:38:14 -0000 +Received: from [69.147.75.180] by t5.bullet.re3.yahoo.com with NNFMP; 06 Aug + 2008 20:38:14 -0000 +Received: from [127.0.0.1] by omp101.mail.re1.yahoo.com with NNFMP; 06 Aug + 2008 20:38:14 -0000 +X-Yahoo-Newman-Id: 778995.62909.bm@omp101.mail.re1.yahoo.com +Received: (qmail 43643 invoked from network); 6 Aug 2008 20:38:14 -0000 +DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; s=s1024; d=yahoo.es; + h=Received:X-YMail-OSG:X-Yahoo-Newman-Property:From:To:Subject:Date:User-Agent:References:In-Reply-To:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-Disposition:Message-Id; + b=ThdHlND5CNUsLPGuk+XhCWkdUA9w7lg4hiAgx8F8egsmQteMpwUlV/Y5tfe6K3O2jzHjtsklkzWqm7WY3VAcxxD/QgxLnianK5ZQHoelDAiGaFRqu8Y42XMZso2ccCBFWUQaKo9C+KIfa3e3ci73qehVxTtmr7bxLjurcSYEBPo= + ; +Received: from unknown (HELO 212251170160.customer.cdi.no) + (juanma_bellon@212.251.170.160 with plain) by smtp109.plus.mail.re1.yahoo.com + with SMTP; 6 Aug 2008 20:38:14 -0000 +X-YMail-OSG: k86L54kVM1kiZbUlYx7gayoBrCLYMFIRDL.KJLBKetNucAbwU4RjeeE1vhjw33hREaUig0CCjG7BTwIfbeZZpRmUcHbxl6gR0z6Sd3lYqA-- +X-Yahoo-Newman-Property: ymail-3 +From: anon@example.com +To: help-gnu-emacs@gnu.org +Date: Wed, 6 Aug 2008 22:38:15 +0200 +User-Agent: KMail/1.9.6 (enterprise 0.20070907.709405) +References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org> + <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org> + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> +In-Reply-To: <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline +Message-Id: <200808062238.15634.juanma_bellon@yahoo.es> +X-detected-kernel: by monty-python.gnu.org: FreeBSD 6.x (1) +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 361 + +On Thursday 31 July 2008, Xah wrote: +> what's the logic of =E2=80=9COK=E2=80=9D? + +=46or all I know, it comes from "0 Knock-outs" (from USA civil war times, +IIRC), i.e., all went really well. + +But this is really off-topic. +=2D-=20 +Juanma + +"Having a smoking section in a restaurant is like + having a peeing section in a swimming pool." + -- Edward Burr + + + + + diff --git a/lib/tests/testdir/new/1220863087.12663_23.mindcrime b/lib/tests/testdir/new/1220863087.12663_23.mindcrime new file mode 100644 index 0000000..ca46f2b --- /dev/null +++ b/lib/tests/testdir/new/1220863087.12663_23.mindcrime @@ -0,0 +1,105 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id C3EF069CB3 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:10 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:10 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs35153wfh; Wed, 6 Aug 2008 + 13:58:17 -0700 (PDT) +Received: by 10.100.166.10 with SMTP id o10mr4182182ane.0.1218056296101; Wed, + 06 Aug 2008 13:58:16 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d34si13875743and.3.2008.08.06.13.58.14; Wed, 06 Aug 2008 + 13:58:16 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; dkim=pass (test + mode) header.i=@gmail.com +Received: from localhost ([127.0.0.1]:33418 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQq5G-0001aY-Cr for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:58:14 -0400 +Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id + 1KQq4n-0001Z9-06 for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:45 -0400 +Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id + 1KQq4l-0001V8-6c for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:44 -0400 +Received: from [199.232.76.173] (port=46438 helo=monty-python.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQq4k-0001Un-V2 for + help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:42 -0400 +Received: from ik-out-1112.google.com ([66.249.90.180]:17562) by + monty-python.gnu.org with esmtp (Exim 4.60) (envelope-from + <lekktu@gmail.com>) id 1KQq4k-0001fk-OW for help-gnu-emacs@gnu.org; Wed, 06 + Aug 2008 16:57:42 -0400 +Received: by ik-out-1112.google.com with SMTP id c21so94956ika.2 for + <help-gnu-emacs@gnu.org>; Wed, 06 Aug 2008 13:57:41 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; + h=domainkey-signature:received:received:message-id:date:from:to + :subject:cc:in-reply-to:mime-version:content-type + :content-transfer-encoding:content-disposition:references; + bh=TTNY9749hpg1+TXOwdaCr+zbQGhBUt3IvsjLWp+pxp0=; + b=BOfudUT/SiW9V4e9+k3dXDzwm+ogdrq4m5OlO+f1H+oE6OAYGIm8dbdqDAOwUewBoS + jRpfZo07YamP9rkko79SeFdQnf7UAPFAw9x7DFCm3x6muSlCcJBR7vYs1rgHOSINAn2B + vQx2//lKR4fXfKNURNu+B30KrvoEmw6m2C8dI= +DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; + h=message-id:date:from:to:subject:cc:in-reply-to:mime-version + :content-type:content-transfer-encoding:content-disposition :references; + b=UMDBulH/LwxDywEH0pfK3DbJ4u2kIZCVDLIM++PqrdcR82HjcS/O3Jhf5OFrf7Fnyj + GH76xmc7zkTG/3aQy2WY6DeWCJaFarEItmhxy3h/xS+kUKeDARzNox0OzK6lIv/u9bdy + f2LnFlYRJ7Q5vy3lxpxAWB4v0qCwtF9LjWFg4= +Received: by 10.210.47.7 with SMTP id u7mr3100239ebu.30.1218056261587; Wed, 06 + Aug 2008 13:57:41 -0700 (PDT) +Received: by 10.210.71.14 with HTTP; Wed, 6 Aug 2008 13:57:41 -0700 (PDT) +Message-ID: <f7ccd24b0808061357t453f5962w8b61f9a453b684d0@mail.gmail.com> +Date: Wed, 6 Aug 2008 22:57:41 +0200 +From: anon@example.com +To: Juanma <juanma_bellon@yahoo.es> +In-Reply-To: <200808062238.15634.juanma_bellon@yahoo.es> +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline +References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org> + <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org> + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> + <200808062238.15634.juanma_bellon@yahoo.es> +X-detected-kernel: by monty-python.gnu.org: Linux 2.6 (newer, 2) +Cc: help-gnu-emacs@gnu.org +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 309 + +On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote: + +> For all I know, it comes from "0 Knock-outs" (from USA civil war times, +> IIRC), i.e., all went really well. + +See http://en.wikipedia.org/wiki/Okay#Etymology + +"0 knock-outs" is among the "Improbable or refuted etymologies". + + Juanma + + diff --git a/lib/tests/testdir/new/1220863087.12663_25.mindcrime b/lib/tests/testdir/new/1220863087.12663_25.mindcrime new file mode 100644 index 0000000..588ace1 --- /dev/null +++ b/lib/tests/testdir/new/1220863087.12663_25.mindcrime @@ -0,0 +1,98 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW, + SPF_PASS autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5 + for <xxxx@localhost>; Fri, 8 Aug 2008 20:56:25 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008 + 07:40:46 -0700 (PDT) +Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri, + 08 Aug 2008 07:40:46 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008 + 07:40:46 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for + xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400 +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Fri, 08 Aug 2008 10:07:30 -0400 +Organization: PANIX Public Access Internet and UNIX, NYC +Message-ID: <uwsireh25.fsf@one.dot.net> +References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org> + <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org> + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> + <200808062238.15634.juanma_bellon@yahoo.es> + <mailman.15958.1218056266.18990.help-gnu-emacs@gnu.org> +NNTP-Posting-Host: panix5.panix.com +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19 + GMT) +X-Complaints-To: abuse@panix.com +NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC) +User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt) +Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs= +Xref: news.stanford.edu gnu.emacs.help:160963 +To: help-gnu-emacs@gnu.org +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 710 +Lines: 27 + +I seem to remember from my early school days it was a campaign slogan +for someone nick-named Kinderhook that went something like + +Old Kinderhook is OK + +- Chris + +"Juanma Barranquero" <lekktu@gmail.com> writes: + +> On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote: +> +>> For all I know, it comes from "0 Knock-outs" (from USA civil war times, +>> IIRC), i.e., all went really well. +> +> See http://en.wikipedia.org/wiki/Okay#Etymology +> +> "0 knock-outs" is among the "Improbable or refuted etymologies". +> +> Juanma +> +> + +-- + (. .) + =ooO=(_)=Ooo===================================== + Chris McMahan | first_initiallastname@one.dot.net + ================================================= + diff --git a/lib/tests/testdir/new/1220863087.12663_9.mindcrime b/lib/tests/testdir/new/1220863087.12663_9.mindcrime new file mode 100644 index 0000000..734ee35 --- /dev/null +++ b/lib/tests/testdir/new/1220863087.12663_9.mindcrime @@ -0,0 +1,209 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-1.2 required=3.0 tests=BAYES_00,HTML_MESSAGE, + MIME_QP_LONG_LINE autolearn=no version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 4E3CF6963B + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:37 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:37 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs94317wfy; Mon, 4 Aug 2008 05:48:28 + -0700 (PDT) +Received: by 10.150.152.17 with SMTP id z17mr1245909ybd.194.1217854107583; + Mon, 04 Aug 2008 05:48:27 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 9si6334793yws.5.2008.08.04.05.47.57; Mon, 04 Aug 2008 05:48:27 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id 4FBC111C6F; Mon, 4 Aug 2008 08:47:54 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from cpsmtpo-eml02.kpnxchange.com (cpsmtpo-eml02.kpnxchange.com + [213.75.38.151]) by sqlite.org (Postfix) with ESMTP id AA4F111C10 for + <sqlite-dev@sqlite.org>; Mon, 4 Aug 2008 08:47:51 -0400 (EDT) +Received: from hpsmtp-eml21.kpnxchange.com ([213.75.38.121]) by + cpsmtpo-eml02.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 + Aug 2008 14:47:50 +0200 +Received: from cpbrm-eml13.kpnsp.local ([195.121.247.250]) by + hpsmtp-eml21.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 + Aug 2008 14:47:50 +0200 +Received: from hpsmtp-eml30.kpnxchange.com ([10.94.53.250]) by + cpbrm-eml13.kpnsp.local with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 Aug + 2008 14:47:50 +0200 +Received: from localhost ([10.94.53.250]) by hpsmtp-eml30.kpnxchange.com with + Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 Aug 2008 14:47:49 +0200 +Content-class: urn:content-classes:message +MIME-Version: 1.0 +X-MimeOLE: Produced By Microsoft Exchange V6.5 +Date: Mon, 4 Aug 2008 14:46:06 +0200 +Message-ID: <F687EC042917A94E8BB4B0902946453AE17D6C@CPEXBE-EML18.kpnsp.local> +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +Thread-Topic: [sqlite-dev] VM optimization inside sqlite3VdbeExec +Thread-Index: Acj2FjkWvteFtLHTTYeVz4ES7E2ggAAGRxeI +References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: anon@example.com +To: <sqlite-dev@sqlite.org> +X-OriginalArrivalTime: 04 Aug 2008 12:47:49.0650 (UTC) + FILETIME=[4D577720:01C8F630] +Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Content-Type: multipart/mixed; boundary="===============1911358387==" +Mime-version: 1.0 +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 5318 + +This is a multi-part message in MIME format. + +--===============1911358387== +Content-class: urn:content-classes:message +Content-Type: multipart/alternative; + boundary="----_=_NextPart_001_01C8F630.0FC2EC1E" + +This is a multi-part message in MIME format. + +------_=_NextPart_001_01C8F630.0FC2EC1E +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Actually, almost every C compiler will already do what you suggest: if = +the range of case labels is compact, the switch will be compiled using a = +jump table. Only if the range is limited and/or sparse other techniques = +will be used, such as linear search and binary search. +=20 +I'm pretty sure, if you perform the tests suggested by Mihai, that you = +will find zero performance difference, neither better, nor worse. +=20 +Paul +=20 +________________________________ + +From: anon@example.com +Sent: Mon 8/4/2008 11:40 AM +To: sqlite-dev@sqlite.org +Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec + + + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the=20 +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html = +<http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html>=20 + +With a properly defined "instructions" array, instead of the switch=20 +statement you can use something like: +goto * instructions[pOp->opcode]; +--- +Marco Bambini +http://www.sqlabs.net <http://www.sqlabs.net/>=20 +http://www.sqlabs.net/blog/ <http://www.sqlabs.net/blog/>=20 +http://www.sqlabs.net/realsqlserver/ = +<http://www.sqlabs.net/realsqlserver/>=20 + + + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev = +<http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>=20 + + + +------_=_NextPart_001_01C8F630.0FC2EC1E +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +<HTML dir=3Dltr><HEAD><TITLE>[sqlite-dev] VM optimization inside = +sqlite3VdbeExec=0A= +=0A= +=0A= +=0A= +
=0A= +
Actually, = +almost every C compiler will already do what you suggest: if the range = +of case labels is compact, the switch will be compiled using a jump = +table. Only if the range is limited and/or sparse other techniques will = +be used, such as linear search and binary search.
=0A= +
 
=0A= +
I'm pretty sure, if you = +perform the tests suggested by Mihai, that you will find zero = +performance difference, neither better, nor worse.
=0A= +
 
=0A= +
Paul
=0A= +
 
=0A= +
=0A= +
=0A= +
=0A= +
From: = +sqlite-dev-bounces@sqlite.org on behalf of Marco Bambini
Sent: = +Mon 8/4/2008 11:40 AM
To: = +sqlite-dev@sqlite.org
Subject: [sqlite-dev] VM optimization = +inside sqlite3VdbeExec

=0A= +
=0A= +

Inside sqlite3VdbeExec there is a very = +big switch statement.
In order to increase performance with few = +modifications to the 
original code, why not use this technique = +?
= +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html<= +/FONT>

With a properly defined = +"instructions" array, instead of the switch 
statement you can = +use something like:
goto * = +instructions[pOp->opcode];
---
Marco Bambini
http://www.sqlabs.net
http://www.sqlabs.net/blog/
http://www.sqlabs.net/realsqlserver/



<= +FONT face=3DArial = +size=3D2>_______________________________________________
sqlite-dev = +mailing list
sqlite-dev@sqlite.org
http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev

+------_=_NextPart_001_01C8F630.0FC2EC1E-- + + +--===============1911358387== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + +--===============1911358387==-- + diff --git a/lib/tests/testdir/tmp/1220863087.12663.ignore b/lib/tests/testdir/tmp/1220863087.12663.ignore new file mode 100644 index 0000000..588ace1 --- /dev/null +++ b/lib/tests/testdir/tmp/1220863087.12663.ignore @@ -0,0 +1,98 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW, + SPF_PASS autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5 + for ; Fri, 8 Aug 2008 20:56:25 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008 + 07:40:46 -0700 (PDT) +Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri, + 08 Aug 2008 07:40:46 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008 + 07:40:46 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for + xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400 +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Fri, 08 Aug 2008 10:07:30 -0400 +Organization: PANIX Public Access Internet and UNIX, NYC +Message-ID: +References: + + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> + <200808062238.15634.juanma_bellon@yahoo.es> + +NNTP-Posting-Host: panix5.panix.com +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19 + GMT) +X-Complaints-To: abuse@panix.com +NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC) +User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt) +Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs= +Xref: news.stanford.edu gnu.emacs.help:160963 +To: help-gnu-emacs@gnu.org +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 710 +Lines: 27 + +I seem to remember from my early school days it was a campaign slogan +for someone nick-named Kinderhook that went something like + +Old Kinderhook is OK + +- Chris + +"Juanma Barranquero" writes: + +> On Wed, Aug 6, 2008 at 22:38, Juanma wrote: +> +>> For all I know, it comes from "0 Knock-outs" (from USA civil war times, +>> IIRC), i.e., all went really well. +> +> See http://en.wikipedia.org/wiki/Okay#Etymology +> +> "0 knock-outs" is among the "Improbable or refuted etymologies". +> +> Juanma +> +> + +-- + (. .) + =ooO=(_)=Ooo===================================== + Chris McMahan | first_initiallastname@one.dot.net + ================================================= + diff --git a/lib/tests/testdir2/Foo/cur/arto.eml b/lib/tests/testdir2/Foo/cur/arto.eml new file mode 100644 index 0000000..ffa0526 --- /dev/null +++ b/lib/tests/testdir2/Foo/cur/arto.eml @@ -0,0 +1,448 @@ +Return-Path: <> +X-Original-To: f00f@localhost +Delivered-To: f00f@localhost +Received: from puppet (puppet [127.0.0.1]) + by f00fmachines.nl (Postfix) with ESMTP id A534D39C7F1 + for ; Mon, 23 May 2011 20:30:05 +0300 (EEST) +Delivered-To: diggler@gmail.com +Received: from ew-in-f109.1e100.net [174.15.27.101] + by puppet with POP3 (fetchmail-6.3.18) + for (single-drop); Mon, 23 May 2011 20:30:05 +0300 (EEST) +Received: by 10.142.147.13 with SMTP id u13cs87252wfd; + Mon, 23 May 2011 01:54:10 -0700 (PDT) +Received: by 10.204.7.74 with SMTP id c10mr1984197bkc.104.1306140849326; + Mon, 23 May 2011 01:54:09 -0700 (PDT) +Received: from MTX4.mbn1.net (mtx4.mbn1.net [213.188.129.252]) + by mx.google.com with ESMTP id e6si18117551bkw.39.2011.05.23.01.54.07; + Mon, 23 May 2011 01:54:08 -0700 (PDT) +Received-SPF: pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) client-ip=213.188.129.252; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) smtp.mail= +Resent-From: +X-Default-Received-SPF: pass (skip=forwardok (res=PASS)) x-ip-name=192.168.10.123; +From: ArtOlive +To: "f00f@f00fmachines.nl" +Reply-To: +Date: Mon, 23 May 2011 10:53:45 +0200 +Subject: NIEUWSBRIEF ART OLIVE | juni exposite in galerie ArtOlive +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d" +X-Mailer: aspNetEmail ver 3.5.2.10 +X-Sender: 87.93.13.24 +X-RemoteIP: 87.93.13.24 +Originating-IP: 87.93.13.24 +X-MAILINGLIJST-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl> +Message-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl> +X-Authenticated-User: guest@mailinglijst.eu +X-STA-Metric: 0 (engine=030) +X-STA-NotSpam: geinformeerd vormen spec:usig:3.8.2 twee samen +X-STA-Spam: 2011 &bull • e-mailing subject:juni +X-BTI-AntiSpam: score:0,sta:0/030,dnsbl:passed,sw:passed,bsn:10/passed,spf:off,bsctr:passed/1,dk:off,pbmf:none,ipr:0/3,trusted:no,ts:no,bs:no,ubl:passed +X-Auto-Response-Suppress: DR, RN, NRN, OOF, AutoReply +Resent-Message-Id: <19740414233016.EB6835A132F5FCF4@MTX4.mbn1.net> +Resent-Date: Mon, 23 May 2011 10:54:07 +0200 (CEST) + +--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d +Content-Type: text/plain; charset="iso-8859-15" +Content-Transfer-Encoding: quoted-printable + +ART-O-NEWS; juni 2011 Westergasfabriekterrein Polonceaukade 17 10= +14 DA Amsterdam tel: 020-6758504 info@artolive.nl www.artolive.nlJuni= + expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers + + +Zondag 5 juni + Elke maand vindt er in de galerie van ArtOlive een expositie plaats. We = +lichten enkele kunstenaars uit (die je misschien al kent van onze website= +), waarbij we een spannende mix van materiaal en techniek presenteren. Ti= +jdens de expositie staan we klaar om elke vraag over ons kunstaanbod te b= +eantwoorden.=20 + +De exposities zijn te bezoeken van maandag t/m vrijdag, tussen 10:00 en 1= +7:00 uur. De opening is altijd op de eerste zondag van de maand. Dit valt= + samen met de Sunday Market die elke maand op het Cultuurpark Westergasfa= +briek georganiseerd wordt. De Sunday Market is gratis te bezoeken en staa= +t vol met kunst, design, mode en heerlijke hapjes, en er hangt altijd een= + vrolijke sfeer. Een ideaal moment dus om in te haken en deze maand twee = +kunstenaars te presenteren: Peter van den Akker en Marinel Vieleers. + +We verwelkomen je graag op zondag 5 juni 2011, van 12:00 t/m 17:00 uur op= + de Polonceaukade 17 van het Cultuurpark Westergasfabriek in Amsterdam!=20= + + + + bekijk meer werk op www.artolive.nl... Peter van den Akker + + +"In mijn beelden en schilderijen staat het mensbeeld centraal; niet als i= +ndividu maar als universele gestalte, waarbij ik op transparante wijze ti= +jdsbeelden en gelaagdheid in het menselijke handelen naar voren breng. Ve= +rhoudingen tussen mensen, verschuivingen in wereldculturen en verandering= +en in techniek, architectuur, natuur en mensbeeld vormen mijn inspiratieb= +ronnen. Het zijn allemaal beelden en sferen die naast en met elkaar besta= +an. Mijn werkwijze omvat vele technieken in verschillende materialen: sch= +ilderijen, gemengde technieken op papier/collages, zeefdrukken, beelden i= +n cortenstaal, keramische objecten." + +Peter van den Akker exposeert regelmatig in binnen- en buitenland bij gal= +erie=EBn en musea en is in verschillende kunstinstellingen en bedrijfscol= +lecties opgenomen. + + + lees meer over Peter... Marinel Vieleers + + +Marinel Vieleers probeert het menselijke in de bouwwerken - en ook vaak i= +ets van het karakter van de bouwer of bewoner - te laten zien. Het zijn m= +aar subtiele details die dat alles weergeven. + +De 'tand des tijds' of invloed van mensen op de gebouwen spelen vaak mee = +in het werk. Koper, cement, lood en andere materialen worden in haar nieu= +we werk gebruikt. Op deze manier kan ze gemakkelijker improviseren en nog= + directer op haar gevoel afgaan. + +Marinel is gefascineerd door de schoonheid van het imperfecte. De gelaagd= +heid van ouderdom, het verval. De imperfectie die ontstaat door toevallig= +e omstandigheden maakt een huis, een muur, een schutting, hout of steen b= +oeiend. Het is doorleefd en het toont een stukje van zijn geschiedenis. + + + lees meer over Marinel... =20 +ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met Marinel Viel= +eers en Peter van den Akker + +Opening op zondag 5 mei, tijdens de Sunday Market op het Cultuurpark West= +ergasfabriek in Amsterdam. Je bent van harte uitgenodigd om tussen 12:00 = +en 17:00 uur langs te komen! + +Daarna is de expositie te zien op werkdagen (ma - vrij) tussen 10:00 en 1= +7:00. De expositie duurt tot 24 juni 2011. + wil je niet langer door artolive ge=EFnformeerd worden? Klik dan hier om= + je af te melden.=20 + kreeg je dit mailtje doorgestuurd en wil je voortaan zelf ook graag de n= +ieuwsbrief ontvangen?=20 + klik dan hier om je aan te melden.=20 + +Deze e-mailing is verzorgd met MailingLijst + +--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d +Content-Type: text/html; charset="iso-8859-15" +Content-Transfer-Encoding: quoted-printable + + + + + + Artolive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
3D"artolive"
ART-O-NEWS • juni 2011 +
Westergasfabriekterrein   Polonceau= +kade 17 1014 DA Amsterdam   tel: 020-6758504  info@artolive.nl<= +/a>  www.artolive.nl
+ + + + + + + +
Juni= + expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers +

Zondag 5 juni
+ Elke maand vindt er in de galerie van Art= +Olive een expositie plaats. We lichten enkele kunstenaars uit (die je mis= +schien al kent van onze website), waarbij we een spannende mix van materi= +aal en techniek presenteren. Tijdens de expositie staan we klaar om elke = +vraag over ons kunstaanbod te beantwoorden.

+

De exposities zijn te bezoeken van maa= +ndag t/m vrijdag, tussen 10:00 en 17:00 uur. De opening is altijd op de e= +erste zondag van de maand. Dit valt samen met de Sunday Market die elke m= +aand op het Cultuurpark Westergasfabriek georganiseerd wordt. De Sunday M= +arket is gratis te bezoeken en staat vol met kunst, design, mode en heerl= +ijke hapjes, en er hangt altijd een vrolijke sfeer. Een ideaal moment dus= + om in te haken en deze maand twee kunstenaars te presenteren: Peter van = +den Akker en Marinel Vieleers.

+

We verwelkomen je graag op zondag 5 ju= +ni 2011, van 12:00 t/m 17:00 uur op de Polonceaukade 17 van het Cultuurpa= +rk Westergasfabriek in Amsterdam!

+
+

3D""  = +bekijk= + meer werk op www.artolive.nl...   

+
+
+ + + + = + + + + + + +
3D""
+ + + + + + + + + + +
<= +a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D= +154043&a=3D10374608&t=3DH"> <= +/td> + Peter van den Akker
+

"In mijn beelden en schild= +erijen staat het mensbeeld centraal; niet als individu maar als universel= +e gestalte, waarbij ik op transparante wijze tijdsbeelden en gelaagdheid = +in het menselijke handelen naar voren breng. Verhoudingen tussen mensen, = +verschuivingen in wereldculturen en veranderingen in techniek, architectu= +ur, natuur en mensbeeld vormen mijn inspiratiebronnen. Het zijn allemaal = +beelden en sferen die naast en met elkaar bestaan. Mijn werkwijze omvat v= +ele technieken in verschillende materialen: schilderijen, gemengde techni= +eken op papier/collages, zeefdrukken, beelden in cortenstaal, keramische = +objecten.”

+

Peter van den Akker expose= +ert regelmatig in binnen- en buitenland bij galerieën en musea en is= + in verschillende kunstinstellingen en bedrijfscollecties opgenomen.

+
+

3D""  = +lees meer over Peter...   

+
= + Marinel Vieleers
+

Marinel Vieleers probeert = +het menselijke in de bouwwerken - en ook vaak iets van het karakter van d= +e bouwer of bewoner - te laten zien. Het zijn maar subtiele details die d= +at alles weergeven.

+

De ‘tand des tijds&r= +squo; of invloed van mensen op de gebouwen spelen vaak mee in het werk. K= +oper, cement, lood en andere materialen worden in haar nieuwe werk gebrui= +kt. Op deze manier kan ze gemakkelijker improviseren en nog directer op h= +aar gevoel afgaan.

+

Marinel is gefascineerd do= +or de schoonheid van het imperfecte. De gelaagdheid van ouderdom, het ver= +val. De imperfectie die ontstaat door toevallige omstandigheden maakt een= + huis, een muur, een schutting, hout of steen boeiend. Het is doorleefd e= +n het toont een stukje van zijn geschiedenis.

+
+

3D""  = +lees meer ov= +er Marinel...   

+
+
+
+ + + + + + + + + +
3D""
+ + + + + + + + + + + +
+
+
+ + + + + + + + + +
3D""
+ + + + + + + + + + + + + + + +

+
ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met = +Marinel Vieleers en Peter van den Akker
+

+
Opening op zondag 5 mei, = +tijdens de Sunday Market op het Cultuurpark Westergasfabriek in Amsterdam= +. Je bent van harte uitgenodigd om tussen 12:00 en 17:00 uur langs te kom= +en!
+

+
Daarna is de expositie te zien op werkdagen (ma - vrij= +) tussen 10:00 en 17:00. De expositie duurt tot 24 juni 2011.
+
+
+
3D"Kunst wil je niet langer door artolive geïnformeerd worden? Klik= + dan hier om je af te melden.
+ kreeg je dit mailtje doorgestuurd en wil je voortaan = +zelf ook graag de nieuwsbrief ontvangen?
+ klik dan hier om= + je aan te melden.
+

<= +HR SIZE=3D1 STYLE=3D"COLOR:#d3d3d3" SIZE=3D1>Deze e-mailing is verzorgd m= +et MailingLijst

+ + +--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d-- diff --git a/lib/tests/testdir2/Foo/cur/fraiche.eml b/lib/tests/testdir2/Foo/cur/fraiche.eml new file mode 100644 index 0000000..c0bf442 --- /dev/null +++ b/lib/tests/testdir2/Foo/cur/fraiche.eml @@ -0,0 +1,10 @@ +From: Sender +To: Recip +Subject: search accents +Date: 2012-12-08 00:48 +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 8bit + +line 1: Глокая куздра штеко будланула бокра и курдячит бокрёнка +line 2: crème fraîche diff --git a/lib/tests/testdir2/Foo/cur/mail5 b/lib/tests/testdir2/Foo/cur/mail5 new file mode 100644 index 0000000..b72195d --- /dev/null +++ b/lib/tests/testdir2/Foo/cur/mail5 @@ -0,0 +1,625 @@ +From: Sitting Bull +To: George Custer +Subject: pics for you +Mail-Reply-To: djcb@djcbsoftware.nl +User-Agent: Hunkpapa/2.15.9 (Almost Unreal) +Message-Id: CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz=bfogtmbGGs5A@mail.gmail.com +Fcc: .sent +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: multipart/mixed; + boundary="Multipart_Sun_Oct_17_10:37:40_2010-1" + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: text/plain; charset=US-ASCII + +Dude! Here are some pics! + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="sittingbull.jpg" +Content-Transfer-Encoding: base64 + +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 +KD//2Q== + + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="custer.jpg" +Content-Transfer-Encoding: base64 + +/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB +AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC +CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox +MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA +//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA +ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g +ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk +LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED +EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B +AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD +REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq +srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB +AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR +B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW +V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC +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 +pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k +A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw +213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe +FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9 +PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo +pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth +eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+ +tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr +P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj +3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5 +OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL +7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz +I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw +LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi +Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg +NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5 +LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog +ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm +Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v +bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu +Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50 +cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg +eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0 +YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv +YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6 +WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS +ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm +OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4 +bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5 +LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy +OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8 +L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg +ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG +BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF +BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e +Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E +AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl +M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA +AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET +2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z +TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww +RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q +KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL +Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8 +IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd +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 + +Cheerio! + +--Multipart_Sun_Oct_17_10:37:40_2010-1-- diff --git a/lib/tests/testdir2/Foo/new/.noindex b/lib/tests/testdir2/Foo/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/tests/testdir2/Foo/tmp/.noindex b/lib/tests/testdir2/Foo/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/tests/testdir2/bar/cur/181736.eml b/lib/tests/testdir2/bar/cur/181736.eml new file mode 100644 index 0000000..56255c4 --- /dev/null +++ b/lib/tests/testdir2/bar/cur/181736.eml @@ -0,0 +1,42 @@ +Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail +X-newsreader: xrn 9.03-beta-14-64bit +Sender: jimbo@lews (Jimbo Foobarcuux) +From: jimbo@slp53.sl.home (Jimbo Foobarcuux) +Reply-To: slp53@pacbell.net +Subject: Re: Are writes "atomic" to readers of the file? +Newsgroups: comp.unix.programmer +References: <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk> +Organization: UseNetServer - www.usenetserver.com +X-Complaints-To: abuse@usenetserver.com +Message-ID: +Date: 08 Mar 2011 17:04:20 GMT +Lines: 27 +Xref: uutiset.elisa.fi comp.unix.programmer:181736 + +John Denver writes: +>Eric the Red wrote: +> +>>> There _IS_ a requirement that all reads and writes to regular files +>>> be atomic. There is also an ordering guarantee. Any implementation +>>> that doesn't provide both atomicity and ordering guarantees is broken. +>> +>> But where is it specified? +> +>The place where it is stated most explicitly is in XSH7 2.9.7 +>Thread Interactions with Regular File Operations: +> +> All of the following functions shall be atomic with respect to each +> other in the effects specified in POSIX.1-2008 when they operate on +> regular files or symbolic links: +> +> [List of functions that includes read() and write()] +> +> If two threads each call one of these functions, each call shall +> either see all of the specified effects of the other call, or none +> of them. +> + +And, for the purposes of this paragraph, the two threads need not be +part of the same process. + +jimbo diff --git a/lib/tests/testdir2/bar/cur/mail1 b/lib/tests/testdir2/bar/cur/mail1 new file mode 100644 index 0000000..56808c6 --- /dev/null +++ b/lib/tests/testdir2/bar/cur/mail1 @@ -0,0 +1,38 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "John Milton" +Subject: Fere libenter homines id quod volunt credunt +To: "Julius Caesar" +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +x-label: Paradise losT +X-Keywords: milton,john +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +OF Mans First Disobedience, and the Fruit +Of that Forbidden Tree, whose mortal tast +Brought Death into the World, and all our woe, +With loss of Eden, till one greater Man +Restore us, and regain the blissful Seat, [ 5 ] +Sing Heav'nly Muse,that on the secret top +Of Oreb, or of Sinai, didst inspire +That Shepherd, who first taught the chosen Seed, +In the Beginning how the Heav'ns and Earth +Rose out of Chaos: Or if Sion Hill [ 10 ] +Delight thee more, and Siloa's Brook that flow'd +Fast by the Oracle of God; I thence +Invoke thy aid to my adventrous Song, +That with no middle flight intends to soar +Above th' Aonian Mount, while it pursues [ 15 ] +Things unattempted yet in Prose or Rhime. +And chiefly Thou O Spirit, that dost prefer +Before all Temples th' upright heart and pure, +Instruct me, for Thou know'st; Thou from the first +Wast present, and with mighty wings outspread [ 20 ] +Dove-like satst brooding on the vast Abyss +And mad'st it pregnant: What in me is dark +Illumin, what is low raise and support; +That to the highth of this great Argument +I may assert Eternal Providence, [ 25 ] +And justifie the wayes of God to men. diff --git a/lib/tests/testdir2/bar/cur/mail2 b/lib/tests/testdir2/bar/cur/mail2 new file mode 100644 index 0000000..3799f30 --- /dev/null +++ b/lib/tests/testdir2/bar/cur/mail2 @@ -0,0 +1,14 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Socrates" +Subject: cool stuff +To: "Alcibiades" +Message-id: <3BE9E6535E0D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +The hour of departure has arrived, and we go our ways—I to die, and you to +live. Which is better God only knows. + +http-emacs diff --git a/lib/tests/testdir2/bar/cur/mail3 b/lib/tests/testdir2/bar/cur/mail3 new file mode 100644 index 0000000..646365e --- /dev/null +++ b/lib/tests/testdir2/bar/cur/mail3 @@ -0,0 +1,34 @@ +From: Napoleon Bonaparte +To: Edmond =?UTF-8?B?RGFudMOocw==?= +Subject: rock on dude +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +Fcc: .sent +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le trois-mâts +le Pharaon, venant de Smyrne, Trieste et Naples. + +Comme d'habitude, un pilote côtier partit aussitôt du port, rasa le château +d'If, et alla aborder le navire entre le cap de Morgion et l'île de Rion. + +Aussitôt, comme d'habitude encore, la plate-forme du fort Saint-Jean s'était +couverte de curieux; car c'est toujours une grande affaire à Marseille que +l'arrivée d'un bâtiment, surtout quand ce bâtiment, comme le Pharaon, a été +construit, gréé, arrimé sur les chantiers de la vieille Phocée, et appartient +à un armateur de la ville. + +Cependant ce bâtiment s'avançait; il avait heureusement franchi le détroit que +quelque secousse volcanique a creusé entre l'île de Calasareigne et l'île de +Jaros; il avait doublé Pomègue, et il s'avançait sous ses trois huniers, son +grand foc et sa brigantine, mais si lentement et d'une allure si triste, que +les curieux, avec cet instinct qui pressent un malheur, se demandaient quel +accident pouvait être arrivé à bord. Néanmoins les experts en navigation +reconnaissaient que si un accident était arrivé, ce ne pouvait être au +bâtiment lui-même; car il s'avançait dans toutes les conditions d'un navire +parfaitement gouverné: son ancre était en mouillage, ses haubans de beaupré +décrochés; et près du pilote, qui s'apprêtait à diriger le Pharaon par +l'étroite entrée du port de Marseille, était un jeune homme au geste rapide et +à l'œil actif, qui surveillait chaque mouvement du navire et répétait chaque +ordre du pilote. diff --git a/lib/tests/testdir2/bar/cur/mail4 b/lib/tests/testdir2/bar/cur/mail4 new file mode 100644 index 0000000..4d21a48 --- /dev/null +++ b/lib/tests/testdir2/bar/cur/mail4 @@ -0,0 +1,29 @@ +Return-Path: +Delivered-To: foo@example.com +Received: from [128.88.204.56] by freemailng0304.web.de with HTTP; + Mon, 07 May 2005 00:27:52 +0200 +Date: Mon, 07 May 2005 00:27:52 +0200 +Message-Id: <293847329847@web.de> +MIME-Version: 1.0 +From: =?iso-8859-1?Q? "=F6tzi" ?= +To: foo@example.com +Subject: =?iso-8859-1?Q?Re:=20der=20b=E4r=20und=20das=20m=E4dchen?= +Precedence: fm-user +Organization: http://freemail.web.de/ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: 8bit +X-MIME-Autoconverted: from quoted-printable to 8bit by mailhost6.ladot.com id j48MScQ30791 +X-Label: \backslash +X-UIDL: 93h!!\i + +123 diff --git a/lib/tests/testdir2/bar/cur/mail6 b/lib/tests/testdir2/bar/cur/mail6 new file mode 100644 index 0000000..c9b799b --- /dev/null +++ b/lib/tests/testdir2/bar/cur/mail6 @@ -0,0 +1,18 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Geoff Tate" +Subject: eyes of a stranger +To: "Enrico Fermi" +Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id> +MIME-version: 1.0 +X-label: @NextActions operation:mindcrime Queensrÿche +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +And I raise my head and stare +Into the eyes of a stranger +I've always known that the mirror never lies +People always turn away +From the eyes of a stranger +Afraid to know what +Lies behind the stare diff --git a/lib/tests/testdir2/bar/new/.noindex b/lib/tests/testdir2/bar/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/tests/testdir2/bar/tmp/.noindex b/lib/tests/testdir2/bar/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/tests/testdir2/wom_bat/cur/atomic b/lib/tests/testdir2/wom_bat/cur/atomic new file mode 100644 index 0000000..c3c6792 --- /dev/null +++ b/lib/tests/testdir2/wom_bat/cur/atomic @@ -0,0 +1,20 @@ +Date: Sat, 12 Nov 2011 12:06:23 -0400 +From: "Richard P. Feynman" +Subject: atoms +To: "Democritus" +Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +If, in some cataclysm, all scientific knowledge were to be destroyed, +and only one sentence passed on to the next generation of creatures, +what statement would contain the most information in the fewest words? +I believe it is the atomic hypothesis (or atomic fact, or whatever you +wish to call it) that all things are made of atoms — little particles +that move around in perpetual motion, attracting each other when they +are a little distance apart, but repelling upon being squeezed into +one another. In that one sentence you will see an enormous amount of +information about the world, if just a little imagination and thinking +are applied. diff --git a/lib/tests/testdir2/wom_bat/cur/rfc822.1 b/lib/tests/testdir2/wom_bat/cur/rfc822.1 new file mode 100644 index 0000000..71c3107 --- /dev/null +++ b/lib/tests/testdir2/wom_bat/cur/rfc822.1 @@ -0,0 +1,44 @@ +Return-Path: +Subject: Fwd: rfc822 +From: foobar +To: martin +Content-Type: multipart/mixed; boundary="=-XHhVx/BCC6tJB87HLPqF" +Message-Id: <1077300332.871.27.camel@example.com> +Mime-Version: 1.0 +X-Mailer: Ximian Evolution 1.4.5 +Date: Fri, 20 Feb 2004 19:05:33 +0100 + +--=-XHhVx/BCC6tJB87HLPqF +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Hello world, forwarding some RFC822 message + +--=-XHhVx/BCC6tJB87HLPqF +Content-Disposition: inline +Content-Type: message/rfc822 + +Return-Path: +Message-ID: <9A01B19D0D605D478E8B72E1367C66340141B9C5@example.com> +From: frob@example.com +To: foo@example.com +Subject: hopjesvla +Date: Sat, 13 Dec 2003 19:35:56 +0100 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: 7bit + +The ship drew on and had safely passed the strait, which some volcanic shock +has made between the Calasareigne and Jaros islands; had doubled Pomegue, and +approached the harbor under topsails, jib, and spanker, but so slowly and +sedately that the idlers, with that instinct which is the forerunner of evil, +asked one another what misfortune could have happened on board. However, those +experienced in navigation saw plainly that if any accident had occurred, it was +not to the vessel herself, for she bore down with all the evidence of being +skilfully handled, the anchor a-cockbill, the jib-boom guys already eased off, +and standing by the side of the pilot, who was steering the Pharaon towards the +narrow entrance of the inner port, was a young man, who, with activity and +vigilant eye, watched every motion of the ship, and repeated each direction of +the pilot. + +--=-XHhVx/BCC6tJB87HLPqF-- diff --git a/lib/tests/testdir2/wom_bat/cur/rfc822.2 b/lib/tests/testdir2/wom_bat/cur/rfc822.2 new file mode 100644 index 0000000..316fa3f --- /dev/null +++ b/lib/tests/testdir2/wom_bat/cur/rfc822.2 @@ -0,0 +1,44 @@ +From: dwarf@siblings.net +To: root@eruditorum.org +Subject: Fwd: test abc +References: <8639ddr9wu.fsf@cthulhu.djcbsoftware> +User-agent: mu 0.98pre; emacs 24.0.91.9 +Date: Thu, 24 Nov 2011 14:24:00 +0200 +Message-ID: <861usxr9nj.fsf@cthulhu.djcbsoftware> +Content-Type: multipart/mixed; boundary="=-=-=" +MIME-Version: 1.0 + +--=-=-= +Content-Type: text/plain + +Saw the website. Am willing to stipulate that you are not RIST 9E03. Suspect +that you are the Dentist, who yearns for honest exchange of views. Anonymous, +digitally signed e-mail is the only safe vehicle for same. + +If you want me to believe you are not the Dentist, provide plausible +explanation for your question regarding why we are building the Crypt. + +Yours truly, + +--=-=-= +Content-Type: message/rfc822 +Content-Disposition: inline; filename= + "1322137188_3.11919.foo:2,S" +Content-Description: rfc822 + +From: dwarf@siblings.net +To: root@eruditorum.org +Subject: test abc +User-agent: mu 0.98pre; emacs 24.0.91.9 +Date: Thu, 24 Nov 2011 14:18:25 +0200 +Message-ID: <8639ddr9wu.fsf@cthulhu.djcbsoftware> +Content-Type: text/plain +MIME-Version: 1.0 + +As I stepped on this unknown middle-aged Filipina's feet during an ill-advised +ballroom dancing foray, she leaned close to me and uttered some latitude and +longitude figures with a conspicuously large number of significant digits of +precision, implying a maximum positional error on the order of the size of a +dinner plate. Gosh, was I ever curious! + +--=-=-=-- diff --git a/lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S b/lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S new file mode 100644 index 0000000..ab1500f --- /dev/null +++ b/lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S @@ -0,0 +1,146 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX, + RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3 + for ; Thu, 7 Aug 2008 08:10:19 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008 + 20:15:17 -0700 (PDT) +Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06 + Aug 2008 20:15:16 -0700 (PDT) +Received: from sourceware.org (sourceware.org [209.132.176.174]) by + mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug + 2008 20:15:16 -0700 (PDT) +Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor + denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + client-ip=209.132.176.174; +Authentication-Results: mx.google.com; spf=neutral (google.com: + 209.132.176.174 is neither permitted nor denied by domain of + gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org +Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000 +Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000 +Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7) + by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000 +Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by + mailgw1a.lmco.com (LM-6) with ESMTP id m773EPZH014730for + ; Wed, 6 Aug 2008 21:14:25 -0600 (MDT) +Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428) + id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 + 21:14:25 -0600 (MDT) +Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF + V6.3-x14 #31428) with ESMTP id <0K5700H5MNNWGX@lmco.com> for + gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT) +Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by + EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed, 06 Aug + 2008 23:14:20 -0400 +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Mickey Mouse" +Subject: gcc include search order +To: "Donald Duck" +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Content-class: urn:content-classes:message +Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm +Precedence: klub +List-Id: +List-Unsubscribe: +List-Archive: +List-Post: +List-Help: +Sender: gcc-help-owner@gcc.gnu.org +Delivered-To: mailing list gcc-help@gcc.gnu.org +Content-Length: 3024 + + +Hi. +In my unit testing I need to change some header files (target is +vxWorks, which supports some things that the sun does not). +So, what I do is fetch the development tree, and then in a new unit test +directory I attempt to compile the unit under test. Since this is NOT +vxworks, I use sed to change some of the .h files and put them in a +./changed directory. + +When I try to compile the file, it is still using the .h file from the +original location, even though I have listed the include path for +./changed before the include path for the development tree. + +Here is a partial output from gcc using the -v option + +GNU CPP version 3.1 (cpplib) (sparc ELF) +GNU C++ version 3.1 (sparc-sun-solaris2.8) + compiled by GNU C version 3.1. +ignoring nonexistent directory "NONE/include" +#include "..." search starts here: +#include <...> search starts here: + . + changed + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface + /usr/local/include/g++-v3 + /usr/local/include/g++-v3/sparc-sun-solaris2.8 + /usr/local/include/g++-v3/backward + /usr/local/include + /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include + /usr/local/sparc-sun-solaris2.8/include + /usr/include +End of search list. + +I know the changed file is correct and that the include is not working +as expected, because when I copy the file from ./changed, back into the +development tree, the compilation works as expected. + +One more bit of information. The source that I cam compiling is in +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app +And it is including files from +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +These include files should be including the files from ./changed (when +they exist) but they are ignoring the .h files in the ./changed +directory and are instead using other, unchanged files in the +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +directory. + +The gcc command line is something like + + TEST_DIR="." + + CHANGED_DIR_NAME=changed + CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME} + + CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I +${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}" + + HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}" + DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST" + + CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow +-Wmissing-prototypes -Wmissing-declarations" + + printf "Compiling the UUT File\n" + gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES} +${AP_APP_FILES}/unitUnderTest.cpp + + +I hope this explanation is clear. If anyone knows how to fix the command +line so that it gets the .h files in the "changed" directory are used +instead of files in the other include directories. + +Thanks +Joe + +---------------------------------------------------- +Time Flies like an Arrow. Fruit Flies like a Banana + + diff --git a/lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S b/lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S new file mode 100644 index 0000000..78efa2a --- /dev/null +++ b/lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S @@ -0,0 +1,77 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3 + for ; Thu, 7 Aug 2008 08:10:08 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008 + 13:40:29 -0700 (PDT) +Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed, + 06 Aug 2008 13:40:28 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008 + 13:40:28 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400 +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Wed, 6 Aug 2008 20:38:35 +0100 +Message-ID: +References: <55dbm5-qcl.ln1@news.ducksburg.com> + +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv +X-Orig-Path: news.ducksburg.com!news +Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94= + sha1:oepBoM0tJBLN52DotWmBBvW5wbg= +User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy) +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail +Xref: news.stanford.edu gnu.emacs.help:160868 +To: help-gnu-emacs@gnu.org +Subject: Re: Learning LISP; Scheme vs elisp. +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 417 +Lines: 11 + +On 2008-08-01, Thien-Thi Nguyen wrote: + +> warriors attack, felling foe after foe, +> few growing old til they realize: to know +> what deceit is worth deflection; +> such receipt reversed rejection! +> then their heavy arms, e'er transformed to shields: +> balanced hooked charms, ploughed deep, rich yields. + +Aha: the exercise for the reader is to place the parens correctly. +Might take me a while to solve this puzzle. + diff --git a/lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S b/lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S new file mode 100644 index 0000000..1e69622 --- /dev/null +++ b/lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S @@ -0,0 +1,22 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime +X-Spam-Level: +Delivered-To: dfgh@floppydisk.nl +Message-ID: <43A09C49.9040902@euler.org> +Date: Wed, 14 Dec 2005 23:27:21 +0100 +From: Fred Flintstone +User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010) +X-Accept-Language: nl-NL, nl, en +MIME-Version: 1.0 +To: dfgh@floppydisk.nl +List-Id: =?utf-8?q?Example_of_List_Id?= +Subject: Re: xyz +References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org> +In-Reply-To: <20051211184308.GB13513@gauss.org> +X-Enigmail-Version: 0.92.0.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-UIDL: T +To: Bilbo Baggins +Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +Fcc: .sent +Organization: The Fellowship of the Ring +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + + +Let's write some fünkÿ text +using umlauts. + +Foo. diff --git a/lib/tests/testdir4/1305664394.2171_402.cthulhu!2, b/lib/tests/testdir4/1305664394.2171_402.cthulhu!2, new file mode 100644 index 0000000..863f714 --- /dev/null +++ b/lib/tests/testdir4/1305664394.2171_402.cthulhu!2, @@ -0,0 +1,17 @@ +From: =?UTF-8?B?TcO8?= +To: Helmut =?UTF-8?B?S3LDtmdlcg==?= +Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +References: +1n-Reply-To: +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + + +Test for issue #38, where apparently searching for accented words in subject, +to etc. fails. + +What about here? Queensrÿche. Mötley Crüe. + + diff --git a/lib/tests/testdir4/181736.eml b/lib/tests/testdir4/181736.eml new file mode 100644 index 0000000..56255c4 --- /dev/null +++ b/lib/tests/testdir4/181736.eml @@ -0,0 +1,42 @@ +Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail +X-newsreader: xrn 9.03-beta-14-64bit +Sender: jimbo@lews (Jimbo Foobarcuux) +From: jimbo@slp53.sl.home (Jimbo Foobarcuux) +Reply-To: slp53@pacbell.net +Subject: Re: Are writes "atomic" to readers of the file? +Newsgroups: comp.unix.programmer +References: <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk> +Organization: UseNetServer - www.usenetserver.com +X-Complaints-To: abuse@usenetserver.com +Message-ID: +Date: 08 Mar 2011 17:04:20 GMT +Lines: 27 +Xref: uutiset.elisa.fi comp.unix.programmer:181736 + +John Denver writes: +>Eric the Red wrote: +> +>>> There _IS_ a requirement that all reads and writes to regular files +>>> be atomic. There is also an ordering guarantee. Any implementation +>>> that doesn't provide both atomicity and ordering guarantees is broken. +>> +>> But where is it specified? +> +>The place where it is stated most explicitly is in XSH7 2.9.7 +>Thread Interactions with Regular File Operations: +> +> All of the following functions shall be atomic with respect to each +> other in the effects specified in POSIX.1-2008 when they operate on +> regular files or symbolic links: +> +> [List of functions that includes read() and write()] +> +> If two threads each call one of these functions, each call shall +> either see all of the specified effects of the other call, or none +> of them. +> + +And, for the purposes of this paragraph, the two threads need not be +part of the same process. + +jimbo diff --git a/lib/tests/testdir4/encrypted!2,S b/lib/tests/testdir4/encrypted!2,S new file mode 100644 index 0000000..b6470e7 --- /dev/null +++ b/lib/tests/testdir4/encrypted!2,S @@ -0,0 +1,57 @@ +Return-path: <> +Envelope-to: peter@example.com +Delivery-date: Fri, 11 May 2012 16:22:03 +0300 +Received: from localhost.example.com ([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-Ux + for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300 +Delivered-To: peter@example.com +From: Brian +To: Peter +Subject: encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:21:42 +0300 +Message-ID: <877gwi97kp.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav +gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d +Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/ +gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v +cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4 +mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy +O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz +gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR +mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym +Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc +4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP +/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l +KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy +gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf +QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref +Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d +gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9 +we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM +qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU +9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY +HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM +0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG +2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ +IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH +=0Paa +-----END PGP MESSAGE----- +--=-=-=-- + diff --git a/lib/tests/testdir4/mail1 b/lib/tests/testdir4/mail1 new file mode 100644 index 0000000..a4e19c1 --- /dev/null +++ b/lib/tests/testdir4/mail1 @@ -0,0 +1,38 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "John Milton" +Subject: Fere libenter homines id quod volunt credunt +To: "Julius Caesar" +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +x-label: Paradise losT +X-keywords: john, milton +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +OF Mans First Disobedience, and the Fruit +Of that Forbidden Tree, whose mortal tast +Brought Death into the World, and all our woe, +With loss of Eden, till one greater Man +Restore us, and regain the blissful Seat, [ 5 ] +Sing Heav'nly Muse,that on the secret top +Of Oreb, or of Sinai, didst inspire +That Shepherd, who first taught the chosen Seed, +In the Beginning how the Heav'ns and Earth +Rose out of Chaos: Or if Sion Hill [ 10 ] +Delight thee more, and Siloa's Brook that flow'd +Fast by the Oracle of God; I thence +Invoke thy aid to my adventrous Song, +That with no middle flight intends to soar +Above th' Aonian Mount, while it pursues [ 15 ] +Things unattempted yet in Prose or Rhime. +And chiefly Thou O Spirit, that dost prefer +Before all Temples th' upright heart and pure, +Instruct me, for Thou know'st; Thou from the first +Wast present, and with mighty wings outspread [ 20 ] +Dove-like satst brooding on the vast Abyss +And mad'st it pregnant: What in me is dark +Illumin, what is low raise and support; +That to the highth of this great Argument +I may assert Eternal Providence, [ 25 ] +And justifie the wayes of God to men. diff --git a/lib/tests/testdir4/mail5 b/lib/tests/testdir4/mail5 new file mode 100644 index 0000000..b12387a --- /dev/null +++ b/lib/tests/testdir4/mail5 @@ -0,0 +1,624 @@ +From: Sitting Bull +To: George Custer +Subject: pics for you +Mail-Reply-To: djcb@djcbsoftware.nl +User-Agent: Hunkpapa/2.15.9 (Almost Unreal) +Fcc: .sent +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: multipart/mixed; + boundary="Multipart_Sun_Oct_17_10:37:40_2010-1" + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: text/plain; charset=US-ASCII + +Dude! Here are some pics! + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="sittingbull.jpg" +Content-Transfer-Encoding: base64 + +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 +KD//2Q== + + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="custer.jpg" +Content-Transfer-Encoding: base64 + +/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB +AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC +CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox +MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA +//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA +ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g +ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk +LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED +EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B +AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD +REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq +srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB +AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR +B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW +V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC +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 +pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k +A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw +213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe +FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9 +PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo +pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth +eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+ +tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr +P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj +3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5 +OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL +7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz +I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw +LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi +Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg +NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5 +LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog +ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm +Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v +bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu +Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50 +cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg +eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0 +YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv +YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6 +WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS +ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm +OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4 +bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5 +LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy +OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8 +L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg +ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG +BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF +BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e +Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E +AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl +M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA +AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET +2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z +TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww +RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q +KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL +Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8 +IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd +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 + +Cheerio! + +--Multipart_Sun_Oct_17_10:37:40_2010-1-- diff --git a/lib/tests/testdir4/multimime!2,FS b/lib/tests/testdir4/multimime!2,FS new file mode 100644 index 0000000..84f85aa --- /dev/null +++ b/lib/tests/testdir4/multimime!2,FS @@ -0,0 +1,27 @@ +Return-path: <> +Envelope-to: djcb@localhost +Delivery-date: Sun, 20 May 2012 09:59:51 +0300 +From: Steve Jobs +To: Bill Gates +Subject: multimime +User-agent: mu4e 0.9.8.4; emacs 23.3.1 +Date: Sat, 19 May 2012 20:57:56 +0100 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain + +abc +--=-=-= +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="test1.C" +Content-Transfer-Encoding: base64 + +aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg== +--=-=-= +Content-Type: text/plain + +def +--=-=-=-- diff --git a/lib/tests/testdir4/signed!2,S b/lib/tests/testdir4/signed!2,S new file mode 100644 index 0000000..7e1319a --- /dev/null +++ b/lib/tests/testdir4/signed!2,S @@ -0,0 +1,36 @@ +User-agent: mu4e 1.1.0; emacs 27.0.50 +From: Skipio +To: Hannibal +Subject: test 123 +Date: Sun, 24 Mar 2019 11:50:42 +0200 +Message-ID: <87zhpky51p.fsf@djcbsoftware.nl> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha256; protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain + + +I am signed! + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCAAdFiEEaYec7RdFk3UPFNqYEd3+qdzEoDYFAlyXUwAACgkQEd3+qdzE +oDbjdw//dAosaEyqSfyUMXjS++iJEeDIwKwO6AjEI0xCbJjHmxq93PA61ApE/BS3 +d/sKa1dsfN+plRS+Fh3NNGSA7evar9dXtMBUr6hwL0VTmm5NDwedaPeuW6mgyVcB +VNUn5x1e/QdnSClapnGd156sryfcM1pg/667fTHT6WC01Xe0sezpkV9l0j4pslYt +y6ud/Hejszax+NcwQY7vkCcVWfB9K4zbiapdoCjHi78S4YAcsbd//KmePOqn04Sa +Tg1XsmMzIh7L/3njkJdIOd9XctTwYEcN5geY1QKrHQ/3+gBeaEYvwsvrnqnVKqMY +WCg/aYibuXl+xNkPMcKHIj1dXA3m5MkL77RrxODiAYz0YkiQx1/DLZs8PV3IVoB4 +f0GGDqyiOwSmSDa4iuCottwO4yG1WM1i7r6pir22qAekIt43wSdwakOrT1IkS8q2 +o0VGiQtEPy27D+ufiw06t02Ryf20Q7i2YcueZxYeRBq41m11M41DJ4wH7LQcJsww +qG5iBOdwQFCTWpi1UrbbFjlxXXWvKMuIU+4k7nsamrEL4SDXmq1v13vtlcgJ6vnn +v7c9+MF7laqdfI+BYnlD1v/9LosPbFTm0hPdvK4yIOORp8Iwj/1PGzTOz6SCUxzA +kDu+Y+NN9/SM1ppStg1OikYPcfEXF8igWhuORwqcmpgHxVkIQ9I= +=wnkU +-----END PGP SIGNATURE----- +--=-=-=-- diff --git a/lib/tests/testdir4/signed-bad!2,S b/lib/tests/testdir4/signed-bad!2,S new file mode 100644 index 0000000..7a37ba9 --- /dev/null +++ b/lib/tests/testdir4/signed-bad!2,S @@ -0,0 +1,35 @@ +Return-path: <> +Envelope-to: skipio@localhost +Delivery-date: Fri, 11 May 2012 16:21:57 +0300 +Received: from localhost.roma.net([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-55 + for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300 +From: Skipio +To: Hannibal +Subject: signed +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:20:45 +0300 +Message-ID: <878vgy97ma.fsf@roma.net> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1; + protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain + + +I am signed! But it's not good because I added this later + +--=-=-= +Content-Type: application/pgp-signature + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p +DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs +=blXz +-----END PGP SIGNATURE----- +--=-=-=-- + diff --git a/lib/tests/testdir4/signed-encrypted!2,S b/lib/tests/testdir4/signed-encrypted!2,S new file mode 100644 index 0000000..a3910e6 --- /dev/null +++ b/lib/tests/testdir4/signed-encrypted!2,S @@ -0,0 +1,54 @@ +Return-path: <> +Envelope-to: karjala@localhost +Delivery-date: Fri, 11 May 2012 16:37:57 +0300 +From: karjala@example.com +To: lapinkulta@example.com +Subject: signed + encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:36:08 +0300 +Message-ID: <874nrm96wn.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16 +uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb +L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J +P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj +cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg +zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW +C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e +YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ +QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA +zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ +GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ +AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v +bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx +ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg +cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM +DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR +w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx +lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs +YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni +Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ +IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI +JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB +t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY +6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr +kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I +C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA= +=pv03 +-----END PGP MESSAGE----- +--=-=-=-- + diff --git a/lib/tests/testdir4/special!2,Sabc b/lib/tests/testdir4/special!2,Sabc new file mode 100644 index 0000000..7f1de8e --- /dev/null +++ b/lib/tests/testdir4/special!2,Sabc @@ -0,0 +1,10 @@ +Date: Thu, 1 Jun 2012 14:57:25 -0200 +From: "Rocky Balboa" +To: "Ivan Drago" +Subject: currying and tail optimization +Message-id: <3BE9E653ef345@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT + +Test 123. I'm a special message with special flags. diff --git a/lib/thirdparty/Makefile.am b/lib/thirdparty/Makefile.am new file mode 100644 index 0000000..7b3af9b --- /dev/null +++ b/lib/thirdparty/Makefile.am @@ -0,0 +1,22 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +EXTRA_DIST= \ + expected.hpp \ + optional.hpp \ + tabulate.hpp diff --git a/lib/thirdparty/expected.hpp b/lib/thirdparty/expected.hpp new file mode 100644 index 0000000..31b130a --- /dev/null +++ b/lib/thirdparty/expected.hpp @@ -0,0 +1,2326 @@ +/// +// expected - An implementation of std::expected with extensions +// Written in 2017 by Simon Brand (simonrbrand@gmail.com, @TartanLlama) +// +// Documentation available at http://tl.tartanllama.xyz/ +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to the +// public domain worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication +// along with this software. If not, see +// . +/// + +#ifndef TL_EXPECTED_HPP +#define TL_EXPECTED_HPP + +#define TL_EXPECTED_VERSION_MAJOR 1 +#define TL_EXPECTED_VERSION_MINOR 0 +#define TL_EXPECTED_VERSION_PATCH 1 + +#include +#include +#include +#include + +#if defined(__EXCEPTIONS) || defined(_CPPUNWIND) +#define TL_EXPECTED_EXCEPTIONS_ENABLED +#endif + +#if (defined(_MSC_VER) && _MSC_VER == 1900) +#define TL_EXPECTED_MSVC2015 +#define TL_EXPECTED_MSVC2015_CONSTEXPR +#else +#define TL_EXPECTED_MSVC2015_CONSTEXPR constexpr +#endif + +#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ + !defined(__clang__)) +#define TL_EXPECTED_GCC49 +#endif + +#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \ + !defined(__clang__)) +#define TL_EXPECTED_GCC54 +#endif + +#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 && \ + !defined(__clang__)) +#define TL_EXPECTED_GCC55 +#endif + +#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ + !defined(__clang__)) +// GCC < 5 doesn't support overloading on const&& for member functions + +#define TL_EXPECTED_NO_CONSTRR +// GCC < 5 doesn't support some standard C++11 type traits +#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ + std::has_trivial_copy_constructor +#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ + std::has_trivial_copy_assign + +// This one will be different for GCC 5.7 if it's ever supported +#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \ + std::is_trivially_destructible + +// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks std::vector +// for non-copyable types +#elif (defined(__GNUC__) && __GNUC__ < 8 && \ + !defined(__clang__)) +#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX +#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX +namespace tl { + namespace detail { + template + struct is_trivially_copy_constructible : std::is_trivially_copy_constructible{}; +#ifdef _GLIBCXX_VECTOR + template + struct is_trivially_copy_constructible> + : std::false_type{}; +#endif + } +} +#endif + +#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ + tl::detail::is_trivially_copy_constructible +#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ + std::is_trivially_copy_assignable +#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible +#else +#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ + std::is_trivially_copy_constructible +#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ + std::is_trivially_copy_assignable +#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \ + std::is_trivially_destructible +#endif + +#if __cplusplus > 201103L +#define TL_EXPECTED_CXX14 +#endif + +#ifdef TL_EXPECTED_GCC49 +#define TL_EXPECTED_GCC49_CONSTEXPR +#else +#define TL_EXPECTED_GCC49_CONSTEXPR constexpr +#endif + +#if (__cplusplus == 201103L || defined(TL_EXPECTED_MSVC2015) || \ + defined(TL_EXPECTED_GCC49)) +#define TL_EXPECTED_11_CONSTEXPR +#else +#define TL_EXPECTED_11_CONSTEXPR constexpr +#endif + +namespace tl { +template class expected; + +#ifndef TL_MONOSTATE_INPLACE_MUTEX +#define TL_MONOSTATE_INPLACE_MUTEX +class monostate {}; + +struct in_place_t { + explicit in_place_t() = default; +}; +static constexpr in_place_t in_place{}; +#endif + +template class unexpected { +public: + static_assert(!std::is_same::value, "E must not be void"); + + unexpected() = delete; + constexpr explicit unexpected(const E &e) : m_val(e) {} + + constexpr explicit unexpected(E &&e) : m_val(std::move(e)) {} + + constexpr const E &value() const & { return m_val; } + TL_EXPECTED_11_CONSTEXPR E &value() & { return m_val; } + TL_EXPECTED_11_CONSTEXPR E &&value() && { return std::move(m_val); } + constexpr const E &&value() const && { return std::move(m_val); } + +private: + E m_val; +}; + +template +constexpr bool operator==(const unexpected &lhs, const unexpected &rhs) { + return lhs.value() == rhs.value(); +} +template +constexpr bool operator!=(const unexpected &lhs, const unexpected &rhs) { + return lhs.value() != rhs.value(); +} +template +constexpr bool operator<(const unexpected &lhs, const unexpected &rhs) { + return lhs.value() < rhs.value(); +} +template +constexpr bool operator<=(const unexpected &lhs, const unexpected &rhs) { + return lhs.value() <= rhs.value(); +} +template +constexpr bool operator>(const unexpected &lhs, const unexpected &rhs) { + return lhs.value() > rhs.value(); +} +template +constexpr bool operator>=(const unexpected &lhs, const unexpected &rhs) { + return lhs.value() >= rhs.value(); +} + +template +unexpected::type> make_unexpected(E &&e) { + return unexpected::type>(std::forward(e)); +} + +struct unexpect_t { + unexpect_t() = default; +}; +static constexpr unexpect_t unexpect{}; + +namespace detail { +template +[[noreturn]] TL_EXPECTED_11_CONSTEXPR void throw_exception(E &&e) { +#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + throw std::forward(e); +#else + #ifdef _MSC_VER + __assume(0); + #else + __builtin_unreachable(); + #endif +#endif +} + +#ifndef TL_TRAITS_MUTEX +#define TL_TRAITS_MUTEX +// C++14-style aliases for brevity +template using remove_const_t = typename std::remove_const::type; +template +using remove_reference_t = typename std::remove_reference::type; +template using decay_t = typename std::decay::type; +template +using enable_if_t = typename std::enable_if::type; +template +using conditional_t = typename std::conditional::type; + +// std::conjunction from C++17 +template struct conjunction : std::true_type {}; +template struct conjunction : B {}; +template +struct conjunction + : std::conditional, B>::type {}; + +#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L +#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND +#endif + +// In C++11 mode, there's an issue in libc++'s std::mem_fn +// which results in a hard-error when using it in a noexcept expression +// in some cases. This is a check to workaround the common failing case. +#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND +template struct is_pointer_to_non_const_member_func : std::false_type {}; +template +struct is_pointer_to_non_const_member_func : std::true_type {}; +template +struct is_pointer_to_non_const_member_func : std::true_type {}; +template +struct is_pointer_to_non_const_member_func : std::true_type {}; +template +struct is_pointer_to_non_const_member_func : std::true_type {}; +template +struct is_pointer_to_non_const_member_func : std::true_type {}; +template +struct is_pointer_to_non_const_member_func : std::true_type {}; + +template struct is_const_or_const_ref : std::false_type {}; +template struct is_const_or_const_ref : std::true_type {}; +template struct is_const_or_const_ref : std::true_type {}; +#endif + +// std::invoke from C++17 +// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround +template ::value + && is_const_or_const_ref::value)>, +#endif + typename = enable_if_t>::value>, + int = 0> + constexpr auto invoke(Fn && f, Args && ... args) noexcept( + noexcept(std::mem_fn(f)(std::forward(args)...))) + -> decltype(std::mem_fn(f)(std::forward(args)...)) { + return std::mem_fn(f)(std::forward(args)...); +} + +template >::value>> + constexpr auto invoke(Fn && f, Args && ... args) noexcept( + noexcept(std::forward(f)(std::forward(args)...))) + -> decltype(std::forward(f)(std::forward(args)...)) { + return std::forward(f)(std::forward(args)...); +} + +// std::invoke_result from C++17 +template struct invoke_result_impl; + +template +struct invoke_result_impl< + F, decltype(detail::invoke(std::declval(), std::declval()...), void()), + Us...> { + using type = decltype(detail::invoke(std::declval(), std::declval()...)); +}; + +template +using invoke_result = invoke_result_impl; + +template +using invoke_result_t = typename invoke_result::type; + +#if defined(_MSC_VER) && _MSC_VER <= 1900 +// TODO make a version which works with MSVC 2015 +template struct is_swappable : std::true_type {}; + +template struct is_nothrow_swappable : std::true_type {}; +#else +// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept +namespace swap_adl_tests { + // if swap ADL finds this then it would call std::swap otherwise (same + // signature) + struct tag {}; + + template tag swap(T&, T&); + template tag swap(T(&a)[N], T(&b)[N]); + + // helper functions to test if an unqualified swap is possible, and if it + // becomes std::swap + template std::false_type can_swap(...) noexcept(false); + template (), std::declval()))> + std::true_type can_swap(int) noexcept(noexcept(swap(std::declval(), + std::declval()))); + + template std::false_type uses_std(...); + template + std::is_same(), std::declval())), tag> + uses_std(int); + + template + struct is_std_swap_noexcept + : std::integral_constant::value&& + std::is_nothrow_move_assignable::value> {}; + + template + struct is_std_swap_noexcept : is_std_swap_noexcept {}; + + template + struct is_adl_swap_noexcept + : std::integral_constant(0))> {}; +} // namespace swap_adl_tests + +template +struct is_swappable + : std::integral_constant< + bool, + decltype(detail::swap_adl_tests::can_swap(0))::value && + (!decltype(detail::swap_adl_tests::uses_std(0))::value || + (std::is_move_assignable::value && + std::is_move_constructible::value))> {}; + +template +struct is_swappable + : std::integral_constant< + bool, + decltype(detail::swap_adl_tests::can_swap(0))::value && + (!decltype( + detail::swap_adl_tests::uses_std(0))::value || + is_swappable::value)> {}; + +template +struct is_nothrow_swappable + : std::integral_constant< + bool, + is_swappable::value && + ((decltype(detail::swap_adl_tests::uses_std(0))::value + && detail::swap_adl_tests::is_std_swap_noexcept::value) || + (!decltype(detail::swap_adl_tests::uses_std(0))::value && + detail::swap_adl_tests::is_adl_swap_noexcept::value))> { +}; +#endif +#endif + +// Trait for checking if a type is a tl::expected +template struct is_expected_impl : std::false_type {}; +template +struct is_expected_impl> : std::true_type {}; +template using is_expected = is_expected_impl>; + +template +using expected_enable_forward_value = detail::enable_if_t< + std::is_constructible::value && + !std::is_same, in_place_t>::value && + !std::is_same, detail::decay_t>::value && + !std::is_same, detail::decay_t>::value>; + +template +using expected_enable_from_other = detail::enable_if_t< + std::is_constructible::value && + std::is_constructible::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value>; + +template +using is_void_or = conditional_t::value, std::true_type, U>; + +template +using is_copy_constructible_or_void = + is_void_or>; + +template +using is_move_constructible_or_void = + is_void_or>; + +template +using is_copy_assignable_or_void = + is_void_or>; + + +template +using is_move_assignable_or_void = + is_void_or>; + + +} // namespace detail + +namespace detail { +struct no_init_t {}; +static constexpr no_init_t no_init{}; + +// Implements the storage of the values, and ensures that the destructor is +// trivial if it can be. +// +// This specialization is for where neither `T` or `E` is trivially +// destructible, so the destructors must be called on destruction of the +// `expected` +template ::value, + bool = std::is_trivially_destructible::value> +struct expected_storage_base { + constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} + constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {} + + template ::value> * = + nullptr> + constexpr expected_storage_base(in_place_t, Args &&... args) + : m_val(std::forward(args)...), m_has_val(true) {} + + template &, Args &&...>::value> * = nullptr> + constexpr expected_storage_base(in_place_t, std::initializer_list il, + Args &&... args) + : m_val(il, std::forward(args)...), m_has_val(true) {} + template ::value> * = + nullptr> + constexpr explicit expected_storage_base(unexpect_t, Args &&... args) + : m_unexpect(std::forward(args)...), m_has_val(false) {} + + template &, Args &&...>::value> * = nullptr> + constexpr explicit expected_storage_base(unexpect_t, + std::initializer_list il, + Args &&... args) + : m_unexpect(il, std::forward(args)...), m_has_val(false) {} + + ~expected_storage_base() { + if (m_has_val) { + m_val.~T(); + } else { + m_unexpect.~unexpected(); + } + } + union { + T m_val; + unexpected m_unexpect; + char m_no_init; + }; + bool m_has_val; +}; + +// This specialization is for when both `T` and `E` are trivially-destructible, +// so the destructor of the `expected` can be trivial. +template struct expected_storage_base { + constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} + constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {} + + template ::value> * = + nullptr> + constexpr expected_storage_base(in_place_t, Args &&... args) + : m_val(std::forward(args)...), m_has_val(true) {} + + template &, Args &&...>::value> * = nullptr> + constexpr expected_storage_base(in_place_t, std::initializer_list il, + Args &&... args) + : m_val(il, std::forward(args)...), m_has_val(true) {} + template ::value> * = + nullptr> + constexpr explicit expected_storage_base(unexpect_t, Args &&... args) + : m_unexpect(std::forward(args)...), m_has_val(false) {} + + template &, Args &&...>::value> * = nullptr> + constexpr explicit expected_storage_base(unexpect_t, + std::initializer_list il, + Args &&... args) + : m_unexpect(il, std::forward(args)...), m_has_val(false) {} + + ~expected_storage_base() = default; + union { + T m_val; + unexpected m_unexpect; + char m_no_init; + }; + bool m_has_val; +}; + +// T is trivial, E is not. +template struct expected_storage_base { + constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} + TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base(no_init_t) + : m_no_init(), m_has_val(false) {} + + template ::value> * = + nullptr> + constexpr expected_storage_base(in_place_t, Args &&... args) + : m_val(std::forward(args)...), m_has_val(true) {} + + template &, Args &&...>::value> * = nullptr> + constexpr expected_storage_base(in_place_t, std::initializer_list il, + Args &&... args) + : m_val(il, std::forward(args)...), m_has_val(true) {} + template ::value> * = + nullptr> + constexpr explicit expected_storage_base(unexpect_t, Args &&... args) + : m_unexpect(std::forward(args)...), m_has_val(false) {} + + template &, Args &&...>::value> * = nullptr> + constexpr explicit expected_storage_base(unexpect_t, + std::initializer_list il, + Args &&... args) + : m_unexpect(il, std::forward(args)...), m_has_val(false) {} + + ~expected_storage_base() { + if (!m_has_val) { + m_unexpect.~unexpected(); + } + } + + union { + T m_val; + unexpected m_unexpect; + char m_no_init; + }; + bool m_has_val; +}; + +// E is trivial, T is not. +template struct expected_storage_base { + constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} + constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {} + + template ::value> * = + nullptr> + constexpr expected_storage_base(in_place_t, Args &&... args) + : m_val(std::forward(args)...), m_has_val(true) {} + + template &, Args &&...>::value> * = nullptr> + constexpr expected_storage_base(in_place_t, std::initializer_list il, + Args &&... args) + : m_val(il, std::forward(args)...), m_has_val(true) {} + template ::value> * = + nullptr> + constexpr explicit expected_storage_base(unexpect_t, Args &&... args) + : m_unexpect(std::forward(args)...), m_has_val(false) {} + + template &, Args &&...>::value> * = nullptr> + constexpr explicit expected_storage_base(unexpect_t, + std::initializer_list il, + Args &&... args) + : m_unexpect(il, std::forward(args)...), m_has_val(false) {} + + ~expected_storage_base() { + if (m_has_val) { + m_val.~T(); + } + } + union { + T m_val; + unexpected m_unexpect; + char m_no_init; + }; + bool m_has_val; +}; + +// `T` is `void`, `E` is trivially-destructible +template struct expected_storage_base { + TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base() : m_has_val(true) {} + constexpr expected_storage_base(no_init_t) : m_val(), m_has_val(false) {} + + constexpr expected_storage_base(in_place_t) : m_has_val(true) {} + + template ::value> * = + nullptr> + constexpr explicit expected_storage_base(unexpect_t, Args &&... args) + : m_unexpect(std::forward(args)...), m_has_val(false) {} + + template &, Args &&...>::value> * = nullptr> + constexpr explicit expected_storage_base(unexpect_t, + std::initializer_list il, + Args &&... args) + : m_unexpect(il, std::forward(args)...), m_has_val(false) {} + + ~expected_storage_base() = default; + struct dummy {}; + union { + unexpected m_unexpect; + dummy m_val; + }; + bool m_has_val; +}; + +// `T` is `void`, `E` is not trivially-destructible +template struct expected_storage_base { + constexpr expected_storage_base() : m_dummy(), m_has_val(true) {} + constexpr expected_storage_base(no_init_t) : m_dummy(), m_has_val(false) {} + + constexpr expected_storage_base(in_place_t) : m_dummy(), m_has_val(true) {} + + template ::value> * = + nullptr> + constexpr explicit expected_storage_base(unexpect_t, Args &&... args) + : m_unexpect(std::forward(args)...), m_has_val(false) {} + + template &, Args &&...>::value> * = nullptr> + constexpr explicit expected_storage_base(unexpect_t, + std::initializer_list il, + Args &&... args) + : m_unexpect(il, std::forward(args)...), m_has_val(false) {} + + ~expected_storage_base() { + if (!m_has_val) { + m_unexpect.~unexpected(); + } + } + + union { + unexpected m_unexpect; + char m_dummy; + }; + bool m_has_val; +}; + +// This base class provides some handy member functions which can be used in +// further derived classes +template +struct expected_operations_base : expected_storage_base { + using expected_storage_base::expected_storage_base; + + template void construct(Args &&... args) noexcept { + new (std::addressof(this->m_val)) T(std::forward(args)...); + this->m_has_val = true; + } + + template void construct_with(Rhs &&rhs) noexcept { + new (std::addressof(this->m_val)) T(std::forward(rhs).get()); + this->m_has_val = true; + } + + template void construct_error(Args &&... args) noexcept { + new (std::addressof(this->m_unexpect)) + unexpected(std::forward(args)...); + this->m_has_val = false; + } + + #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + + // These assign overloads ensure that the most efficient assignment + // implementation is used while maintaining the strong exception guarantee. + // The problematic case is where rhs has a value, but *this does not. + // + // This overload handles the case where we can just copy-construct `T` + // directly into place without throwing. + template ::value> + * = nullptr> + void assign(const expected_operations_base &rhs) noexcept { + if (!this->m_has_val && rhs.m_has_val) { + geterr().~unexpected(); + construct(rhs.get()); + } else { + assign_common(rhs); + } + } + + // This overload handles the case where we can attempt to create a copy of + // `T`, then no-throw move it into place if the copy was successful. + template ::value && + std::is_nothrow_move_constructible::value> + * = nullptr> + void assign(const expected_operations_base &rhs) noexcept { + if (!this->m_has_val && rhs.m_has_val) { + T tmp = rhs.get(); + geterr().~unexpected(); + construct(std::move(tmp)); + } else { + assign_common(rhs); + } + } + + // This overload is the worst-case, where we have to move-construct the + // unexpected value into temporary storage, then try to copy the T into place. + // If the construction succeeds, then everything is fine, but if it throws, + // then we move the old unexpected value back into place before rethrowing the + // exception. + template ::value && + !std::is_nothrow_move_constructible::value> + * = nullptr> + void assign(const expected_operations_base &rhs) { + if (!this->m_has_val && rhs.m_has_val) { + auto tmp = std::move(geterr()); + geterr().~unexpected(); + +#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + try { + construct(rhs.get()); + } catch (...) { + geterr() = std::move(tmp); + throw; + } +#else + construct(rhs.get()); +#endif + } else { + assign_common(rhs); + } + } + + // These overloads do the same as above, but for rvalues + template ::value> + * = nullptr> + void assign(expected_operations_base &&rhs) noexcept { + if (!this->m_has_val && rhs.m_has_val) { + geterr().~unexpected(); + construct(std::move(rhs).get()); + } else { + assign_common(std::move(rhs)); + } + } + + template ::value> + * = nullptr> + void assign(expected_operations_base &&rhs) { + if (!this->m_has_val && rhs.m_has_val) { + auto tmp = std::move(geterr()); + geterr().~unexpected(); +#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + try { + construct(std::move(rhs).get()); + } catch (...) { + geterr() = std::move(tmp); + throw; + } +#else + construct(std::move(rhs).get()); +#endif + } else { + assign_common(std::move(rhs)); + } + } + + #else + + // If exceptions are disabled then we can just copy-construct + void assign(const expected_operations_base &rhs) noexcept { + if (!this->m_has_val && rhs.m_has_val) { + geterr().~unexpected(); + construct(rhs.get()); + } else { + assign_common(rhs); + } + } + + void assign(expected_operations_base &&rhs) noexcept { + if (!this->m_has_val && rhs.m_has_val) { + geterr().~unexpected(); + construct(std::move(rhs).get()); + } else { + assign_common(rhs); + } + } + + #endif + + // The common part of move/copy assigning + template void assign_common(Rhs &&rhs) { + if (this->m_has_val) { + if (rhs.m_has_val) { + get() = std::forward(rhs).get(); + } else { + destroy_val(); + construct_error(std::forward(rhs).geterr()); + } + } else { + if (!rhs.m_has_val) { + geterr() = std::forward(rhs).geterr(); + } + } + } + + bool has_value() const { return this->m_has_val; } + + TL_EXPECTED_11_CONSTEXPR T &get() & { return this->m_val; } + constexpr const T &get() const & { return this->m_val; } + TL_EXPECTED_11_CONSTEXPR T &&get() && { return std::move(this->m_val); } +#ifndef TL_EXPECTED_NO_CONSTRR + constexpr const T &&get() const && { return std::move(this->m_val); } +#endif + + TL_EXPECTED_11_CONSTEXPR unexpected &geterr() & { + return this->m_unexpect; + } + constexpr const unexpected &geterr() const & { return this->m_unexpect; } + TL_EXPECTED_11_CONSTEXPR unexpected &&geterr() && { + return std::move(this->m_unexpect); + } +#ifndef TL_EXPECTED_NO_CONSTRR + constexpr const unexpected &&geterr() const && { + return std::move(this->m_unexpect); + } +#endif + + TL_EXPECTED_11_CONSTEXPR void destroy_val() { + get().~T(); + } +}; + +// This base class provides some handy member functions which can be used in +// further derived classes +template +struct expected_operations_base : expected_storage_base { + using expected_storage_base::expected_storage_base; + + template void construct() noexcept { this->m_has_val = true; } + + // This function doesn't use its argument, but needs it so that code in + // levels above this can work independently of whether T is void + template void construct_with(Rhs &&) noexcept { + this->m_has_val = true; + } + + template void construct_error(Args &&... args) noexcept { + new (std::addressof(this->m_unexpect)) + unexpected(std::forward(args)...); + this->m_has_val = false; + } + + template void assign(Rhs &&rhs) noexcept { + if (!this->m_has_val) { + if (rhs.m_has_val) { + geterr().~unexpected(); + construct(); + } else { + geterr() = std::forward(rhs).geterr(); + } + } else { + if (!rhs.m_has_val) { + construct_error(std::forward(rhs).geterr()); + } + } + } + + bool has_value() const { return this->m_has_val; } + + TL_EXPECTED_11_CONSTEXPR unexpected &geterr() & { + return this->m_unexpect; + } + constexpr const unexpected &geterr() const & { return this->m_unexpect; } + TL_EXPECTED_11_CONSTEXPR unexpected &&geterr() && { + return std::move(this->m_unexpect); + } +#ifndef TL_EXPECTED_NO_CONSTRR + constexpr const unexpected &&geterr() const && { + return std::move(this->m_unexpect); + } +#endif + + TL_EXPECTED_11_CONSTEXPR void destroy_val() { + //no-op + } +}; + +// This class manages conditionally having a trivial copy constructor +// This specialization is for when T and E are trivially copy constructible +template :: + value &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value> +struct expected_copy_base : expected_operations_base { + using expected_operations_base::expected_operations_base; +}; + +// This specialization is for when T or E are not trivially copy constructible +template +struct expected_copy_base : expected_operations_base { + using expected_operations_base::expected_operations_base; + + expected_copy_base() = default; + expected_copy_base(const expected_copy_base &rhs) + : expected_operations_base(no_init) { + if (rhs.has_value()) { + this->construct_with(rhs); + } else { + this->construct_error(rhs.geterr()); + } + } + + expected_copy_base(expected_copy_base &&rhs) = default; + expected_copy_base &operator=(const expected_copy_base &rhs) = default; + expected_copy_base &operator=(expected_copy_base &&rhs) = default; +}; + +// This class manages conditionally having a trivial move constructor +// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it +// doesn't implement an analogue to std::is_trivially_move_constructible. We +// have to make do with a non-trivial move constructor even if T is trivially +// move constructible +#ifndef TL_EXPECTED_GCC49 +template >::value + &&std::is_trivially_move_constructible::value> +struct expected_move_base : expected_copy_base { + using expected_copy_base::expected_copy_base; +}; +#else +template struct expected_move_base; +#endif +template +struct expected_move_base : expected_copy_base { + using expected_copy_base::expected_copy_base; + + expected_move_base() = default; + expected_move_base(const expected_move_base &rhs) = default; + + expected_move_base(expected_move_base &&rhs) noexcept( + std::is_nothrow_move_constructible::value) + : expected_copy_base(no_init) { + if (rhs.has_value()) { + this->construct_with(std::move(rhs)); + } else { + this->construct_error(std::move(rhs.geterr())); + } + } + expected_move_base &operator=(const expected_move_base &rhs) = default; + expected_move_base &operator=(expected_move_base &&rhs) = default; +}; + +// This class manages conditionally having a trivial copy assignment operator +template >::value + &&TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(E)::value + &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value + &&TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(E)::value> +struct expected_copy_assign_base : expected_move_base { + using expected_move_base::expected_move_base; +}; + +template +struct expected_copy_assign_base : expected_move_base { + using expected_move_base::expected_move_base; + + expected_copy_assign_base() = default; + expected_copy_assign_base(const expected_copy_assign_base &rhs) = default; + + expected_copy_assign_base(expected_copy_assign_base &&rhs) = default; + expected_copy_assign_base &operator=(const expected_copy_assign_base &rhs) { + this->assign(rhs); + return *this; + } + expected_copy_assign_base & + operator=(expected_copy_assign_base &&rhs) = default; +}; + +// This class manages conditionally having a trivial move assignment operator +// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it +// doesn't implement an analogue to std::is_trivially_move_assignable. We have +// to make do with a non-trivial move assignment operator even if T is trivially +// move assignable +#ifndef TL_EXPECTED_GCC49 +template , + std::is_trivially_move_constructible, + std::is_trivially_move_assignable>>:: + value &&std::is_trivially_destructible::value + &&std::is_trivially_move_constructible::value + &&std::is_trivially_move_assignable::value> +struct expected_move_assign_base : expected_copy_assign_base { + using expected_copy_assign_base::expected_copy_assign_base; +}; +#else +template struct expected_move_assign_base; +#endif + +template +struct expected_move_assign_base + : expected_copy_assign_base { + using expected_copy_assign_base::expected_copy_assign_base; + + expected_move_assign_base() = default; + expected_move_assign_base(const expected_move_assign_base &rhs) = default; + + expected_move_assign_base(expected_move_assign_base &&rhs) = default; + + expected_move_assign_base & + operator=(const expected_move_assign_base &rhs) = default; + + expected_move_assign_base & + operator=(expected_move_assign_base &&rhs) noexcept( + std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_assignable::value) { + this->assign(std::move(rhs)); + return *this; + } +}; + +// expected_delete_ctor_base will conditionally delete copy and move +// constructors depending on whether T is copy/move constructible +template ::value && + std::is_copy_constructible::value), + bool EnableMove = (is_move_constructible_or_void::value && + std::is_move_constructible::value)> +struct expected_delete_ctor_base { + expected_delete_ctor_base() = default; + expected_delete_ctor_base(const expected_delete_ctor_base &) = default; + expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default; + expected_delete_ctor_base & + operator=(const expected_delete_ctor_base &) = default; + expected_delete_ctor_base & + operator=(expected_delete_ctor_base &&) noexcept = default; +}; + +template +struct expected_delete_ctor_base { + expected_delete_ctor_base() = default; + expected_delete_ctor_base(const expected_delete_ctor_base &) = default; + expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete; + expected_delete_ctor_base & + operator=(const expected_delete_ctor_base &) = default; + expected_delete_ctor_base & + operator=(expected_delete_ctor_base &&) noexcept = default; +}; + +template +struct expected_delete_ctor_base { + expected_delete_ctor_base() = default; + expected_delete_ctor_base(const expected_delete_ctor_base &) = delete; + expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default; + expected_delete_ctor_base & + operator=(const expected_delete_ctor_base &) = default; + expected_delete_ctor_base & + operator=(expected_delete_ctor_base &&) noexcept = default; +}; + +template +struct expected_delete_ctor_base { + expected_delete_ctor_base() = default; + expected_delete_ctor_base(const expected_delete_ctor_base &) = delete; + expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete; + expected_delete_ctor_base & + operator=(const expected_delete_ctor_base &) = default; + expected_delete_ctor_base & + operator=(expected_delete_ctor_base &&) noexcept = default; +}; + +// expected_delete_assign_base will conditionally delete copy and move +// constructors depending on whether T and E are copy/move constructible + +// assignable +template ::value && + std::is_copy_constructible::value && + is_copy_assignable_or_void::value && + std::is_copy_assignable::value), + bool EnableMove = (is_move_constructible_or_void::value && + std::is_move_constructible::value && + is_move_assignable_or_void::value && + std::is_move_assignable::value)> +struct expected_delete_assign_base { + expected_delete_assign_base() = default; + expected_delete_assign_base(const expected_delete_assign_base &) = default; + expected_delete_assign_base(expected_delete_assign_base &&) noexcept = + default; + expected_delete_assign_base & + operator=(const expected_delete_assign_base &) = default; + expected_delete_assign_base & + operator=(expected_delete_assign_base &&) noexcept = default; +}; + +template +struct expected_delete_assign_base { + expected_delete_assign_base() = default; + expected_delete_assign_base(const expected_delete_assign_base &) = default; + expected_delete_assign_base(expected_delete_assign_base &&) noexcept = + default; + expected_delete_assign_base & + operator=(const expected_delete_assign_base &) = default; + expected_delete_assign_base & + operator=(expected_delete_assign_base &&) noexcept = delete; +}; + +template +struct expected_delete_assign_base { + expected_delete_assign_base() = default; + expected_delete_assign_base(const expected_delete_assign_base &) = default; + expected_delete_assign_base(expected_delete_assign_base &&) noexcept = + default; + expected_delete_assign_base & + operator=(const expected_delete_assign_base &) = delete; + expected_delete_assign_base & + operator=(expected_delete_assign_base &&) noexcept = default; +}; + +template +struct expected_delete_assign_base { + expected_delete_assign_base() = default; + expected_delete_assign_base(const expected_delete_assign_base &) = default; + expected_delete_assign_base(expected_delete_assign_base &&) noexcept = + default; + expected_delete_assign_base & + operator=(const expected_delete_assign_base &) = delete; + expected_delete_assign_base & + operator=(expected_delete_assign_base &&) noexcept = delete; +}; + +// This is needed to be able to construct the expected_default_ctor_base which +// follows, while still conditionally deleting the default constructor. +struct default_constructor_tag { + explicit constexpr default_constructor_tag() = default; +}; + +// expected_default_ctor_base will ensure that expected has a deleted default +// consturctor if T is not default constructible. +// This specialization is for when T is default constructible +template ::value || std::is_void::value> +struct expected_default_ctor_base { + constexpr expected_default_ctor_base() noexcept = default; + constexpr expected_default_ctor_base( + expected_default_ctor_base const &) noexcept = default; + constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept = + default; + expected_default_ctor_base & + operator=(expected_default_ctor_base const &) noexcept = default; + expected_default_ctor_base & + operator=(expected_default_ctor_base &&) noexcept = default; + + constexpr explicit expected_default_ctor_base(default_constructor_tag) {} +}; + +// This specialization is for when T is not default constructible +template struct expected_default_ctor_base { + constexpr expected_default_ctor_base() noexcept = delete; + constexpr expected_default_ctor_base( + expected_default_ctor_base const &) noexcept = default; + constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept = + default; + expected_default_ctor_base & + operator=(expected_default_ctor_base const &) noexcept = default; + expected_default_ctor_base & + operator=(expected_default_ctor_base &&) noexcept = default; + + constexpr explicit expected_default_ctor_base(default_constructor_tag) {} +}; +} // namespace detail + +template class bad_expected_access : public std::exception { +public: + explicit bad_expected_access(E e) : m_val(std::move(e)) {} + + virtual const char *what() const noexcept override { + return "Bad expected access"; + } + + const E &error() const & { return m_val; } + E &error() & { return m_val; } + const E &&error() const && { return std::move(m_val); } + E &&error() && { return std::move(m_val); } + +private: + E m_val; +}; + +/// An `expected` object is an object that contains the storage for +/// another object and manages the lifetime of this contained object `T`. +/// Alternatively it could contain the storage for another unexpected object +/// `E`. The contained object may not be initialized after the expected object +/// has been initialized, and may not be destroyed before the expected object +/// has been destroyed. The initialization state of the contained object is +/// tracked by the expected object. +template +class expected : private detail::expected_move_assign_base, + private detail::expected_delete_ctor_base, + private detail::expected_delete_assign_base, + private detail::expected_default_ctor_base { + static_assert(!std::is_reference::value, "T must not be a reference"); + static_assert(!std::is_same::type>::value, + "T must not be in_place_t"); + static_assert(!std::is_same::type>::value, + "T must not be unexpect_t"); + static_assert(!std::is_same>::type>::value, + "T must not be unexpected"); + static_assert(!std::is_reference::value, "E must not be a reference"); + + T *valptr() { return std::addressof(this->m_val); } + const T *valptr() const { return std::addressof(this->m_val); } + unexpected *errptr() { return std::addressof(this->m_unexpect); } + const unexpected *errptr() const { return std::addressof(this->m_unexpect); } + + template ::value> * = nullptr> + TL_EXPECTED_11_CONSTEXPR U &val() { + return this->m_val; + } + TL_EXPECTED_11_CONSTEXPR unexpected &err() { return this->m_unexpect; } + + template ::value> * = nullptr> + constexpr const U &val() const { + return this->m_val; + } + constexpr const unexpected &err() const { return this->m_unexpect; } + + using impl_base = detail::expected_move_assign_base; + using ctor_base = detail::expected_default_ctor_base; + +public: + typedef T value_type; + typedef E error_type; + typedef unexpected unexpected_type; + +#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ + !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) + template TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) & { + return and_then_impl(*this, std::forward(f)); + } + template TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && { + return and_then_impl(std::move(*this), std::forward(f)); + } + template constexpr auto and_then(F &&f) const & { + return and_then_impl(*this, std::forward(f)); + } + +#ifndef TL_EXPECTED_NO_CONSTRR + template constexpr auto and_then(F &&f) const && { + return and_then_impl(std::move(*this), std::forward(f)); + } +#endif + +#else + template + TL_EXPECTED_11_CONSTEXPR auto + and_then(F &&f) & -> decltype(and_then_impl(std::declval(), std::forward(f))) { + return and_then_impl(*this, std::forward(f)); + } + template + TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && -> decltype( + and_then_impl(std::declval(), std::forward(f))) { + return and_then_impl(std::move(*this), std::forward(f)); + } + template + constexpr auto and_then(F &&f) const & -> decltype( + and_then_impl(std::declval(), std::forward(f))) { + return and_then_impl(*this, std::forward(f)); + } + +#ifndef TL_EXPECTED_NO_CONSTRR + template + constexpr auto and_then(F &&f) const && -> decltype( + and_then_impl(std::declval(), std::forward(f))) { + return and_then_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + +#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ + !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) + template TL_EXPECTED_11_CONSTEXPR auto map(F &&f) & { + return expected_map_impl(*this, std::forward(f)); + } + template TL_EXPECTED_11_CONSTEXPR auto map(F &&f) && { + return expected_map_impl(std::move(*this), std::forward(f)); + } + template constexpr auto map(F &&f) const & { + return expected_map_impl(*this, std::forward(f)); + } + template constexpr auto map(F &&f) const && { + return expected_map_impl(std::move(*this), std::forward(f)); + } +#else + template + TL_EXPECTED_11_CONSTEXPR decltype( + expected_map_impl(std::declval(), std::declval())) + map(F &&f) & { + return expected_map_impl(*this, std::forward(f)); + } + template + TL_EXPECTED_11_CONSTEXPR decltype( + expected_map_impl(std::declval(), std::declval())) + map(F &&f) && { + return expected_map_impl(std::move(*this), std::forward(f)); + } + template + constexpr decltype(expected_map_impl(std::declval(), + std::declval())) + map(F &&f) const & { + return expected_map_impl(*this, std::forward(f)); + } + +#ifndef TL_EXPECTED_NO_CONSTRR + template + constexpr decltype(expected_map_impl(std::declval(), + std::declval())) + map(F &&f) const && { + return expected_map_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + +#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ + !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) + template TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) & { + return expected_map_impl(*this, std::forward(f)); + } + template TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) && { + return expected_map_impl(std::move(*this), std::forward(f)); + } + template constexpr auto transform(F &&f) const & { + return expected_map_impl(*this, std::forward(f)); + } + template constexpr auto transform(F &&f) const && { + return expected_map_impl(std::move(*this), std::forward(f)); + } +#else + template + TL_EXPECTED_11_CONSTEXPR decltype( + expected_map_impl(std::declval(), std::declval())) + transform(F &&f) & { + return expected_map_impl(*this, std::forward(f)); + } + template + TL_EXPECTED_11_CONSTEXPR decltype( + expected_map_impl(std::declval(), std::declval())) + transform(F &&f) && { + return expected_map_impl(std::move(*this), std::forward(f)); + } + template + constexpr decltype(expected_map_impl(std::declval(), + std::declval())) + transform(F &&f) const & { + return expected_map_impl(*this, std::forward(f)); + } + +#ifndef TL_EXPECTED_NO_CONSTRR + template + constexpr decltype(expected_map_impl(std::declval(), + std::declval())) + transform(F &&f) const && { + return expected_map_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + +#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ + !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) + template TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) & { + return map_error_impl(*this, std::forward(f)); + } + template TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) && { + return map_error_impl(std::move(*this), std::forward(f)); + } + template constexpr auto map_error(F &&f) const & { + return map_error_impl(*this, std::forward(f)); + } + template constexpr auto map_error(F &&f) const && { + return map_error_impl(std::move(*this), std::forward(f)); + } +#else + template + TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval(), + std::declval())) + map_error(F &&f) & { + return map_error_impl(*this, std::forward(f)); + } + template + TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval(), + std::declval())) + map_error(F &&f) && { + return map_error_impl(std::move(*this), std::forward(f)); + } + template + constexpr decltype(map_error_impl(std::declval(), + std::declval())) + map_error(F &&f) const & { + return map_error_impl(*this, std::forward(f)); + } + +#ifndef TL_EXPECTED_NO_CONSTRR + template + constexpr decltype(map_error_impl(std::declval(), + std::declval())) + map_error(F &&f) const && { + return map_error_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + template expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) & { + return or_else_impl(*this, std::forward(f)); + } + + template expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) && { + return or_else_impl(std::move(*this), std::forward(f)); + } + + template expected constexpr or_else(F &&f) const & { + return or_else_impl(*this, std::forward(f)); + } + +#ifndef TL_EXPECTED_NO_CONSTRR + template expected constexpr or_else(F &&f) const && { + return or_else_impl(std::move(*this), std::forward(f)); + } +#endif + constexpr expected() = default; + constexpr expected(const expected &rhs) = default; + constexpr expected(expected &&rhs) = default; + expected &operator=(const expected &rhs) = default; + expected &operator=(expected &&rhs) = default; + + template ::value> * = + nullptr> + constexpr expected(in_place_t, Args &&... args) + : impl_base(in_place, std::forward(args)...), + ctor_base(detail::default_constructor_tag{}) {} + + template &, Args &&...>::value> * = nullptr> + constexpr expected(in_place_t, std::initializer_list il, Args &&... args) + : impl_base(in_place, il, std::forward(args)...), + ctor_base(detail::default_constructor_tag{}) {} + + template ::value> * = + nullptr, + detail::enable_if_t::value> * = + nullptr> + explicit constexpr expected(const unexpected &e) + : impl_base(unexpect, e.value()), + ctor_base(detail::default_constructor_tag{}) {} + + template < + class G = E, + detail::enable_if_t::value> * = + nullptr, + detail::enable_if_t::value> * = nullptr> + constexpr expected(unexpected const &e) + : impl_base(unexpect, e.value()), + ctor_base(detail::default_constructor_tag{}) {} + + template < + class G = E, + detail::enable_if_t::value> * = nullptr, + detail::enable_if_t::value> * = nullptr> + explicit constexpr expected(unexpected &&e) noexcept( + std::is_nothrow_constructible::value) + : impl_base(unexpect, std::move(e.value())), + ctor_base(detail::default_constructor_tag{}) {} + + template < + class G = E, + detail::enable_if_t::value> * = nullptr, + detail::enable_if_t::value> * = nullptr> + constexpr expected(unexpected &&e) noexcept( + std::is_nothrow_constructible::value) + : impl_base(unexpect, std::move(e.value())), + ctor_base(detail::default_constructor_tag{}) {} + + template ::value> * = + nullptr> + constexpr explicit expected(unexpect_t, Args &&... args) + : impl_base(unexpect, std::forward(args)...), + ctor_base(detail::default_constructor_tag{}) {} + + template &, Args &&...>::value> * = nullptr> + constexpr explicit expected(unexpect_t, std::initializer_list il, + Args &&... args) + : impl_base(unexpect, il, std::forward(args)...), + ctor_base(detail::default_constructor_tag{}) {} + + template ::value && + std::is_convertible::value)> * = + nullptr, + detail::expected_enable_from_other + * = nullptr> + explicit TL_EXPECTED_11_CONSTEXPR expected(const expected &rhs) + : ctor_base(detail::default_constructor_tag{}) { + if (rhs.has_value()) { + this->construct(*rhs); + } else { + this->construct_error(rhs.error()); + } + } + + template ::value && + std::is_convertible::value)> * = + nullptr, + detail::expected_enable_from_other + * = nullptr> + TL_EXPECTED_11_CONSTEXPR expected(const expected &rhs) + : ctor_base(detail::default_constructor_tag{}) { + if (rhs.has_value()) { + this->construct(*rhs); + } else { + this->construct_error(rhs.error()); + } + } + + template < + class U, class G, + detail::enable_if_t::value && + std::is_convertible::value)> * = nullptr, + detail::expected_enable_from_other * = nullptr> + explicit TL_EXPECTED_11_CONSTEXPR expected(expected &&rhs) + : ctor_base(detail::default_constructor_tag{}) { + if (rhs.has_value()) { + this->construct(std::move(*rhs)); + } else { + this->construct_error(std::move(rhs.error())); + } + } + + template < + class U, class G, + detail::enable_if_t<(std::is_convertible::value && + std::is_convertible::value)> * = nullptr, + detail::expected_enable_from_other * = nullptr> + TL_EXPECTED_11_CONSTEXPR expected(expected &&rhs) + : ctor_base(detail::default_constructor_tag{}) { + if (rhs.has_value()) { + this->construct(std::move(*rhs)); + } else { + this->construct_error(std::move(rhs.error())); + } + } + + template < + class U = T, + detail::enable_if_t::value> * = nullptr, + detail::expected_enable_forward_value * = nullptr> + explicit TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v) + : expected(in_place, std::forward(v)) {} + + template < + class U = T, + detail::enable_if_t::value> * = nullptr, + detail::expected_enable_forward_value * = nullptr> + TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v) + : expected(in_place, std::forward(v)) {} + + template < + class U = T, class G = T, + detail::enable_if_t::value> * = + nullptr, + detail::enable_if_t::value> * = nullptr, + detail::enable_if_t< + (!std::is_same, detail::decay_t>::value && + !detail::conjunction, + std::is_same>>::value && + std::is_constructible::value && + std::is_assignable::value && + std::is_nothrow_move_constructible::value)> * = nullptr> + expected &operator=(U &&v) { + if (has_value()) { + val() = std::forward(v); + } else { + err().~unexpected(); + ::new (valptr()) T(std::forward(v)); + this->m_has_val = true; + } + + return *this; + } + + template < + class U = T, class G = T, + detail::enable_if_t::value> * = + nullptr, + detail::enable_if_t::value> * = nullptr, + detail::enable_if_t< + (!std::is_same, detail::decay_t>::value && + !detail::conjunction, + std::is_same>>::value && + std::is_constructible::value && + std::is_assignable::value && + std::is_nothrow_move_constructible::value)> * = nullptr> + expected &operator=(U &&v) { + if (has_value()) { + val() = std::forward(v); + } else { + auto tmp = std::move(err()); + err().~unexpected(); + + #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + try { + ::new (valptr()) T(std::forward(v)); + this->m_has_val = true; + } catch (...) { + err() = std::move(tmp); + throw; + } + #else + ::new (valptr()) T(std::forward(v)); + this->m_has_val = true; + #endif + } + + return *this; + } + + template ::value && + std::is_assignable::value> * = nullptr> + expected &operator=(const unexpected &rhs) { + if (!has_value()) { + err() = rhs; + } else { + this->destroy_val(); + ::new (errptr()) unexpected(rhs); + this->m_has_val = false; + } + + return *this; + } + + template ::value && + std::is_move_assignable::value> * = nullptr> + expected &operator=(unexpected &&rhs) noexcept { + if (!has_value()) { + err() = std::move(rhs); + } else { + this->destroy_val(); + ::new (errptr()) unexpected(std::move(rhs)); + this->m_has_val = false; + } + + return *this; + } + + template ::value> * = nullptr> + void emplace(Args &&... args) { + if (has_value()) { + val() = T(std::forward(args)...); + } else { + err().~unexpected(); + ::new (valptr()) T(std::forward(args)...); + this->m_has_val = true; + } + } + + template ::value> * = nullptr> + void emplace(Args &&... args) { + if (has_value()) { + val() = T(std::forward(args)...); + } else { + auto tmp = std::move(err()); + err().~unexpected(); + + #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + try { + ::new (valptr()) T(std::forward(args)...); + this->m_has_val = true; + } catch (...) { + err() = std::move(tmp); + throw; + } + #else + ::new (valptr()) T(std::forward(args)...); + this->m_has_val = true; + #endif + } + } + + template &, Args &&...>::value> * = nullptr> + void emplace(std::initializer_list il, Args &&... args) { + if (has_value()) { + T t(il, std::forward(args)...); + val() = std::move(t); + } else { + err().~unexpected(); + ::new (valptr()) T(il, std::forward(args)...); + this->m_has_val = true; + } + } + + template &, Args &&...>::value> * = nullptr> + void emplace(std::initializer_list il, Args &&... args) { + if (has_value()) { + T t(il, std::forward(args)...); + val() = std::move(t); + } else { + auto tmp = std::move(err()); + err().~unexpected(); + + #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + try { + ::new (valptr()) T(il, std::forward(args)...); + this->m_has_val = true; + } catch (...) { + err() = std::move(tmp); + throw; + } + #else + ::new (valptr()) T(il, std::forward(args)...); + this->m_has_val = true; + #endif + } + } + +private: + using t_is_void = std::true_type; + using t_is_not_void = std::false_type; + using t_is_nothrow_move_constructible = std::true_type; + using move_constructing_t_can_throw = std::false_type; + using e_is_nothrow_move_constructible = std::true_type; + using move_constructing_e_can_throw = std::false_type; + + void swap_where_both_have_value(expected &/*rhs*/ , t_is_void) noexcept { + // swapping void is a no-op + } + + void swap_where_both_have_value(expected &rhs, t_is_not_void) { + using std::swap; + swap(val(), rhs.val()); + } + + void swap_where_only_one_has_value(expected &rhs, t_is_void) noexcept( + std::is_nothrow_move_constructible::value) { + ::new (errptr()) unexpected_type(std::move(rhs.err())); + rhs.err().~unexpected_type(); + std::swap(this->m_has_val, rhs.m_has_val); + } + + void swap_where_only_one_has_value(expected &rhs, t_is_not_void) { + swap_where_only_one_has_value_and_t_is_not_void( + rhs, typename std::is_nothrow_move_constructible::type{}, + typename std::is_nothrow_move_constructible::type{}); + } + + void swap_where_only_one_has_value_and_t_is_not_void( + expected &rhs, t_is_nothrow_move_constructible, + e_is_nothrow_move_constructible) noexcept { + auto temp = std::move(val()); + val().~T(); + ::new (errptr()) unexpected_type(std::move(rhs.err())); + rhs.err().~unexpected_type(); + ::new (rhs.valptr()) T(std::move(temp)); + std::swap(this->m_has_val, rhs.m_has_val); + } + + void swap_where_only_one_has_value_and_t_is_not_void( + expected &rhs, t_is_nothrow_move_constructible, + move_constructing_e_can_throw) { + auto temp = std::move(val()); + val().~T(); +#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + try { + ::new (errptr()) unexpected_type(std::move(rhs.err())); + rhs.err().~unexpected_type(); + ::new (rhs.valptr()) T(std::move(temp)); + std::swap(this->m_has_val, rhs.m_has_val); + } catch (...) { + val() = std::move(temp); + throw; + } +#else + ::new (errptr()) unexpected_type(std::move(rhs.err())); + rhs.err().~unexpected_type(); + ::new (rhs.valptr()) T(std::move(temp)); + std::swap(this->m_has_val, rhs.m_has_val); +#endif + } + + void swap_where_only_one_has_value_and_t_is_not_void( + expected &rhs, move_constructing_t_can_throw, + t_is_nothrow_move_constructible) { + auto temp = std::move(rhs.err()); + rhs.err().~unexpected_type(); +#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED + try { + ::new (rhs.valptr()) T(val()); + val().~T(); + ::new (errptr()) unexpected_type(std::move(temp)); + std::swap(this->m_has_val, rhs.m_has_val); + } catch (...) { + rhs.err() = std::move(temp); + throw; + } +#else + ::new (rhs.valptr()) T(val()); + val().~T(); + ::new (errptr()) unexpected_type(std::move(temp)); + std::swap(this->m_has_val, rhs.m_has_val); +#endif + } + +public: + template + detail::enable_if_t::value && + detail::is_swappable::value && + (std::is_nothrow_move_constructible::value || + std::is_nothrow_move_constructible::value)> + swap(expected &rhs) noexcept( + std::is_nothrow_move_constructible::value + &&detail::is_nothrow_swappable::value + &&std::is_nothrow_move_constructible::value + &&detail::is_nothrow_swappable::value) { + if (has_value() && rhs.has_value()) { + swap_where_both_have_value(rhs, typename std::is_void::type{}); + } else if (!has_value() && rhs.has_value()) { + rhs.swap(*this); + } else if (has_value()) { + swap_where_only_one_has_value(rhs, typename std::is_void::type{}); + } else { + using std::swap; + swap(err(), rhs.err()); + } + } + + constexpr const T *operator->() const { return valptr(); } + TL_EXPECTED_11_CONSTEXPR T *operator->() { return valptr(); } + + template ::value> * = nullptr> + constexpr const U &operator*() const & { + return val(); + } + template ::value> * = nullptr> + TL_EXPECTED_11_CONSTEXPR U &operator*() & { + return val(); + } + template ::value> * = nullptr> + constexpr const U &&operator*() const && { + return std::move(val()); + } + template ::value> * = nullptr> + TL_EXPECTED_11_CONSTEXPR U &&operator*() && { + return std::move(val()); + } + + constexpr bool has_value() const noexcept { return this->m_has_val; } + constexpr explicit operator bool() const noexcept { return this->m_has_val; } + + template ::value> * = nullptr> + TL_EXPECTED_11_CONSTEXPR const U &value() const & { + if (!has_value()) + detail::throw_exception(bad_expected_access(err().value())); + return val(); + } + template ::value> * = nullptr> + TL_EXPECTED_11_CONSTEXPR U &value() & { + if (!has_value()) + detail::throw_exception(bad_expected_access(err().value())); + return val(); + } + template ::value> * = nullptr> + TL_EXPECTED_11_CONSTEXPR const U &&value() const && { + if (!has_value()) + detail::throw_exception(bad_expected_access(std::move(err()).value())); + return std::move(val()); + } + template ::value> * = nullptr> + TL_EXPECTED_11_CONSTEXPR U &&value() && { + if (!has_value()) + detail::throw_exception(bad_expected_access(std::move(err()).value())); + return std::move(val()); + } + + constexpr const E &error() const & { return err().value(); } + TL_EXPECTED_11_CONSTEXPR E &error() & { return err().value(); } + constexpr const E &&error() const && { return std::move(err().value()); } + TL_EXPECTED_11_CONSTEXPR E &&error() && { return std::move(err().value()); } + + template constexpr T value_or(U &&v) const & { + static_assert(std::is_copy_constructible::value && + std::is_convertible::value, + "T must be copy-constructible and convertible to from U&&"); + return bool(*this) ? **this : static_cast(std::forward(v)); + } + template TL_EXPECTED_11_CONSTEXPR T value_or(U &&v) && { + static_assert(std::is_move_constructible::value && + std::is_convertible::value, + "T must be move-constructible and convertible to from U&&"); + return bool(*this) ? std::move(**this) : static_cast(std::forward(v)); + } +}; + +namespace detail { +template using exp_t = typename detail::decay_t::value_type; +template using err_t = typename detail::decay_t::error_type; +template using ret_t = expected>; + +#ifdef TL_EXPECTED_CXX14 +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + *std::declval()))> +constexpr auto and_then_impl(Exp &&exp, F &&f) { + static_assert(detail::is_expected::value, "F must return an expected"); + + return exp.has_value() + ? detail::invoke(std::forward(f), *std::forward(exp)) + : Ret(unexpect, std::forward(exp).error()); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval()))> +constexpr auto and_then_impl(Exp &&exp, F &&f) { + static_assert(detail::is_expected::value, "F must return an expected"); + + return exp.has_value() ? detail::invoke(std::forward(f)) + : Ret(unexpect, std::forward(exp).error()); +} +#else +template struct TC; +template (), + *std::declval())), + detail::enable_if_t>::value> * = nullptr> +auto and_then_impl(Exp &&exp, F &&f) -> Ret { + static_assert(detail::is_expected::value, "F must return an expected"); + + return exp.has_value() + ? detail::invoke(std::forward(f), *std::forward(exp)) + : Ret(unexpect, std::forward(exp).error()); +} + +template ())), + detail::enable_if_t>::value> * = nullptr> +constexpr auto and_then_impl(Exp &&exp, F &&f) -> Ret { + static_assert(detail::is_expected::value, "F must return an expected"); + + return exp.has_value() ? detail::invoke(std::forward(f)) + : Ret(unexpect, std::forward(exp).error()); +} +#endif + +#ifdef TL_EXPECTED_CXX14 +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + *std::declval())), + detail::enable_if_t::value> * = nullptr> +constexpr auto expected_map_impl(Exp &&exp, F &&f) { + using result = ret_t>; + return exp.has_value() ? result(detail::invoke(std::forward(f), + *std::forward(exp))) + : result(unexpect, std::forward(exp).error()); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + *std::declval())), + detail::enable_if_t::value> * = nullptr> +auto expected_map_impl(Exp &&exp, F &&f) { + using result = expected>; + if (exp.has_value()) { + detail::invoke(std::forward(f), *std::forward(exp)); + return result(); + } + + return result(unexpect, std::forward(exp).error()); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval())), + detail::enable_if_t::value> * = nullptr> +constexpr auto expected_map_impl(Exp &&exp, F &&f) { + using result = ret_t>; + return exp.has_value() ? result(detail::invoke(std::forward(f))) + : result(unexpect, std::forward(exp).error()); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval())), + detail::enable_if_t::value> * = nullptr> +auto expected_map_impl(Exp &&exp, F &&f) { + using result = expected>; + if (exp.has_value()) { + detail::invoke(std::forward(f)); + return result(); + } + + return result(unexpect, std::forward(exp).error()); +} +#else +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + *std::declval())), + detail::enable_if_t::value> * = nullptr> + +constexpr auto expected_map_impl(Exp &&exp, F &&f) + -> ret_t> { + using result = ret_t>; + + return exp.has_value() ? result(detail::invoke(std::forward(f), + *std::forward(exp))) + : result(unexpect, std::forward(exp).error()); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + *std::declval())), + detail::enable_if_t::value> * = nullptr> + +auto expected_map_impl(Exp &&exp, F &&f) -> expected> { + if (exp.has_value()) { + detail::invoke(std::forward(f), *std::forward(exp)); + return {}; + } + + return unexpected>(std::forward(exp).error()); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval())), + detail::enable_if_t::value> * = nullptr> + +constexpr auto expected_map_impl(Exp &&exp, F &&f) + -> ret_t> { + using result = ret_t>; + + return exp.has_value() ? result(detail::invoke(std::forward(f))) + : result(unexpect, std::forward(exp).error()); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval())), + detail::enable_if_t::value> * = nullptr> + +auto expected_map_impl(Exp &&exp, F &&f) -> expected> { + if (exp.has_value()) { + detail::invoke(std::forward(f)); + return {}; + } + + return unexpected>(std::forward(exp).error()); +} +#endif + +#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ + !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +constexpr auto map_error_impl(Exp &&exp, F &&f) { + using result = expected, detail::decay_t>; + return exp.has_value() + ? result(*std::forward(exp)) + : result(unexpect, detail::invoke(std::forward(f), + std::forward(exp).error())); +} +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +auto map_error_impl(Exp &&exp, F &&f) { + using result = expected, monostate>; + if (exp.has_value()) { + return result(*std::forward(exp)); + } + + detail::invoke(std::forward(f), std::forward(exp).error()); + return result(unexpect, monostate{}); +} +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +constexpr auto map_error_impl(Exp &&exp, F &&f) { + using result = expected, detail::decay_t>; + return exp.has_value() + ? result() + : result(unexpect, detail::invoke(std::forward(f), + std::forward(exp).error())); +} +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +auto map_error_impl(Exp &&exp, F &&f) { + using result = expected, monostate>; + if (exp.has_value()) { + return result(); + } + + detail::invoke(std::forward(f), std::forward(exp).error()); + return result(unexpect, monostate{}); +} +#else +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +constexpr auto map_error_impl(Exp &&exp, F &&f) + -> expected, detail::decay_t> { + using result = expected, detail::decay_t>; + + return exp.has_value() + ? result(*std::forward(exp)) + : result(unexpect, detail::invoke(std::forward(f), + std::forward(exp).error())); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +auto map_error_impl(Exp &&exp, F &&f) -> expected, monostate> { + using result = expected, monostate>; + if (exp.has_value()) { + return result(*std::forward(exp)); + } + + detail::invoke(std::forward(f), std::forward(exp).error()); + return result(unexpect, monostate{}); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +constexpr auto map_error_impl(Exp &&exp, F &&f) + -> expected, detail::decay_t> { + using result = expected, detail::decay_t>; + + return exp.has_value() + ? result() + : result(unexpect, detail::invoke(std::forward(f), + std::forward(exp).error())); +} + +template >::value> * = nullptr, + class Ret = decltype(detail::invoke(std::declval(), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +auto map_error_impl(Exp &&exp, F &&f) -> expected, monostate> { + using result = expected, monostate>; + if (exp.has_value()) { + return result(); + } + + detail::invoke(std::forward(f), std::forward(exp).error()); + return result(unexpect, monostate{}); +} +#endif + +#ifdef TL_EXPECTED_CXX14 +template (), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +constexpr auto or_else_impl(Exp &&exp, F &&f) { + static_assert(detail::is_expected::value, "F must return an expected"); + return exp.has_value() + ? std::forward(exp) + : detail::invoke(std::forward(f), std::forward(exp).error()); +} + +template (), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +detail::decay_t or_else_impl(Exp &&exp, F &&f) { + return exp.has_value() + ? std::forward(exp) + : (detail::invoke(std::forward(f), std::forward(exp).error()), + std::forward(exp)); +} +#else +template (), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +auto or_else_impl(Exp &&exp, F &&f) -> Ret { + static_assert(detail::is_expected::value, "F must return an expected"); + return exp.has_value() + ? std::forward(exp) + : detail::invoke(std::forward(f), std::forward(exp).error()); +} + +template (), + std::declval().error())), + detail::enable_if_t::value> * = nullptr> +detail::decay_t or_else_impl(Exp &&exp, F &&f) { + return exp.has_value() + ? std::forward(exp) + : (detail::invoke(std::forward(f), std::forward(exp).error()), + std::forward(exp)); +} +#endif +} // namespace detail + +template +constexpr bool operator==(const expected &lhs, + const expected &rhs) { + return (lhs.has_value() != rhs.has_value()) + ? false + : (!lhs.has_value() ? lhs.error() == rhs.error() : *lhs == *rhs); +} +template +constexpr bool operator!=(const expected &lhs, + const expected &rhs) { + return (lhs.has_value() != rhs.has_value()) + ? true + : (!lhs.has_value() ? lhs.error() != rhs.error() : *lhs != *rhs); +} + +template +constexpr bool operator==(const expected &x, const U &v) { + return x.has_value() ? *x == v : false; +} +template +constexpr bool operator==(const U &v, const expected &x) { + return x.has_value() ? *x == v : false; +} +template +constexpr bool operator!=(const expected &x, const U &v) { + return x.has_value() ? *x != v : true; +} +template +constexpr bool operator!=(const U &v, const expected &x) { + return x.has_value() ? *x != v : true; +} + +template +constexpr bool operator==(const expected &x, const unexpected &e) { + return x.has_value() ? false : x.error() == e.value(); +} +template +constexpr bool operator==(const unexpected &e, const expected &x) { + return x.has_value() ? false : x.error() == e.value(); +} +template +constexpr bool operator!=(const expected &x, const unexpected &e) { + return x.has_value() ? true : x.error() != e.value(); +} +template +constexpr bool operator!=(const unexpected &e, const expected &x) { + return x.has_value() ? true : x.error() != e.value(); +} + +template ::value || + std::is_move_constructible::value) && + detail::is_swappable::value && + std::is_move_constructible::value && + detail::is_swappable::value> * = nullptr> +void swap(expected &lhs, + expected &rhs) noexcept(noexcept(lhs.swap(rhs))) { + lhs.swap(rhs); +} +} // namespace tl + +#endif diff --git a/lib/thirdparty/optional.hpp b/lib/thirdparty/optional.hpp new file mode 100644 index 0000000..37b774a --- /dev/null +++ b/lib/thirdparty/optional.hpp @@ -0,0 +1,2063 @@ + +/// +// optional - An implementation of std::optional with extensions +// Written in 2017 by Sy Brand (tartanllama@gmail.com, @TartanLlama) +// +// Documentation available at https://tl.tartanllama.xyz/ +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to the +// public domain worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication +// along with this software. If not, see +// . +/// + +#ifndef TL_OPTIONAL_HPP +#define TL_OPTIONAL_HPP + +#define TL_OPTIONAL_VERSION_MAJOR 1 +#define TL_OPTIONAL_VERSION_MINOR 0 +#define TL_OPTIONAL_VERSION_PATCH 0 + +#include +#include +#include +#include +#include + +#if (defined(_MSC_VER) && _MSC_VER == 1900) +#define TL_OPTIONAL_MSVC2015 +#endif + +#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ + !defined(__clang__)) +#define TL_OPTIONAL_GCC49 +#endif + +#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \ + !defined(__clang__)) +#define TL_OPTIONAL_GCC54 +#endif + +#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 && \ + !defined(__clang__)) +#define TL_OPTIONAL_GCC55 +#endif + +#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ + !defined(__clang__)) +// GCC < 5 doesn't support overloading on const&& for member functions +#define TL_OPTIONAL_NO_CONSTRR + +// GCC < 5 doesn't support some standard C++11 type traits +#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ + std::has_trivial_copy_constructor::value +#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) std::has_trivial_copy_assign::value + +// This one will be different for GCC 5.7 if it's ever supported +#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible::value + +// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks std::vector +// for non-copyable types +#elif (defined(__GNUC__) && __GNUC__ < 8 && \ + !defined(__clang__)) +#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX +#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX +namespace tl { + namespace detail { + template + struct is_trivially_copy_constructible : std::is_trivially_copy_constructible{}; +#ifdef _GLIBCXX_VECTOR + template + struct is_trivially_copy_constructible> + : std::is_trivially_copy_constructible{}; +#endif + } +} +#endif + +#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ + tl::detail::is_trivially_copy_constructible::value +#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ + std::is_trivially_copy_assignable::value +#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible::value +#else +#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ + std::is_trivially_copy_constructible::value +#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ + std::is_trivially_copy_assignable::value +#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible::value +#endif + +#if __cplusplus > 201103L +#define TL_OPTIONAL_CXX14 +#endif + +// constexpr implies const in C++11, not C++14 +#if (__cplusplus == 201103L || defined(TL_OPTIONAL_MSVC2015) || \ + defined(TL_OPTIONAL_GCC49)) +#define TL_OPTIONAL_11_CONSTEXPR +#else +#define TL_OPTIONAL_11_CONSTEXPR constexpr +#endif + +namespace tl { +#ifndef TL_MONOSTATE_INPLACE_MUTEX +#define TL_MONOSTATE_INPLACE_MUTEX +/// Used to represent an optional with no data; essentially a bool +class monostate {}; + +/// A tag type to tell optional to construct its value in-place +struct in_place_t { + explicit in_place_t() = default; +}; +/// A tag to tell optional to construct its value in-place +static constexpr in_place_t in_place{}; +#endif + +template class optional; + +namespace detail { +#ifndef TL_TRAITS_MUTEX +#define TL_TRAITS_MUTEX +// C++14-style aliases for brevity +template using remove_const_t = typename std::remove_const::type; +template +using remove_reference_t = typename std::remove_reference::type; +template using decay_t = typename std::decay::type; +template +using enable_if_t = typename std::enable_if::type; +template +using conditional_t = typename std::conditional::type; + +// std::conjunction from C++17 +template struct conjunction : std::true_type {}; +template struct conjunction : B {}; +template +struct conjunction + : std::conditional, B>::type {}; + +#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L +#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND +#endif + +// In C++11 mode, there's an issue in libc++'s std::mem_fn +// which results in a hard-error when using it in a noexcept expression +// in some cases. This is a check to workaround the common failing case. +#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND +template struct is_pointer_to_non_const_member_func : std::false_type{}; +template +struct is_pointer_to_non_const_member_func : std::true_type{}; +template +struct is_pointer_to_non_const_member_func : std::true_type{}; +template +struct is_pointer_to_non_const_member_func : std::true_type{}; +template +struct is_pointer_to_non_const_member_func : std::true_type{}; +template +struct is_pointer_to_non_const_member_func : std::true_type{}; +template +struct is_pointer_to_non_const_member_func : std::true_type{}; + +template struct is_const_or_const_ref : std::false_type{}; +template struct is_const_or_const_ref : std::true_type{}; +template struct is_const_or_const_ref : std::true_type{}; +#endif + +// std::invoke from C++17 +// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround +template ::value + && is_const_or_const_ref::value)>, +#endif + typename = enable_if_t>::value>, + int = 0> +constexpr auto invoke(Fn &&f, Args &&... args) noexcept( + noexcept(std::mem_fn(f)(std::forward(args)...))) + -> decltype(std::mem_fn(f)(std::forward(args)...)) { + return std::mem_fn(f)(std::forward(args)...); +} + +template >::value>> +constexpr auto invoke(Fn &&f, Args &&... args) noexcept( + noexcept(std::forward(f)(std::forward(args)...))) + -> decltype(std::forward(f)(std::forward(args)...)) { + return std::forward(f)(std::forward(args)...); +} + +// std::invoke_result from C++17 +template struct invoke_result_impl; + +template +struct invoke_result_impl< + F, decltype(detail::invoke(std::declval(), std::declval()...), void()), + Us...> { + using type = decltype(detail::invoke(std::declval(), std::declval()...)); +}; + +template +using invoke_result = invoke_result_impl; + +template +using invoke_result_t = typename invoke_result::type; + +#if defined(_MSC_VER) && _MSC_VER <= 1900 +// TODO make a version which works with MSVC 2015 +template struct is_swappable : std::true_type {}; + +template struct is_nothrow_swappable : std::true_type {}; +#else +// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept +namespace swap_adl_tests { +// if swap ADL finds this then it would call std::swap otherwise (same +// signature) +struct tag {}; + +template tag swap(T &, T &); +template tag swap(T (&a)[N], T (&b)[N]); + +// helper functions to test if an unqualified swap is possible, and if it +// becomes std::swap +template std::false_type can_swap(...) noexcept(false); +template (), std::declval()))> +std::true_type can_swap(int) noexcept(noexcept(swap(std::declval(), + std::declval()))); + +template std::false_type uses_std(...); +template +std::is_same(), std::declval())), tag> +uses_std(int); + +template +struct is_std_swap_noexcept + : std::integral_constant::value && + std::is_nothrow_move_assignable::value> {}; + +template +struct is_std_swap_noexcept : is_std_swap_noexcept {}; + +template +struct is_adl_swap_noexcept + : std::integral_constant(0))> {}; +} // namespace swap_adl_tests + +template +struct is_swappable + : std::integral_constant< + bool, + decltype(detail::swap_adl_tests::can_swap(0))::value && + (!decltype(detail::swap_adl_tests::uses_std(0))::value || + (std::is_move_assignable::value && + std::is_move_constructible::value))> {}; + +template +struct is_swappable + : std::integral_constant< + bool, + decltype(detail::swap_adl_tests::can_swap(0))::value && + (!decltype( + detail::swap_adl_tests::uses_std(0))::value || + is_swappable::value)> {}; + +template +struct is_nothrow_swappable + : std::integral_constant< + bool, + is_swappable::value && + ((decltype(detail::swap_adl_tests::uses_std(0))::value + &&detail::swap_adl_tests::is_std_swap_noexcept::value) || + (!decltype(detail::swap_adl_tests::uses_std(0))::value && + detail::swap_adl_tests::is_adl_swap_noexcept::value))> { +}; +#endif +#endif + +// std::void_t from C++17 +template struct voider { using type = void; }; +template using void_t = typename voider::type; + +// Trait for checking if a type is a tl::optional +template struct is_optional_impl : std::false_type {}; +template struct is_optional_impl> : std::true_type {}; +template using is_optional = is_optional_impl>; + +// Change void to tl::monostate +template +using fixup_void = conditional_t::value, monostate, U>; + +template > +using get_map_return = optional>>; + +// Check if invoking F for some Us returns void +template struct returns_void_impl; +template +struct returns_void_impl>, U...> + : std::is_void> {}; +template +using returns_void = returns_void_impl; + +template +using enable_if_ret_void = enable_if_t::value>; + +template +using disable_if_ret_void = enable_if_t::value>; + +template +using enable_forward_value = + detail::enable_if_t::value && + !std::is_same, in_place_t>::value && + !std::is_same, detail::decay_t>::value>; + +template +using enable_from_other = detail::enable_if_t< + std::is_constructible::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value>; + +template +using enable_assign_forward = detail::enable_if_t< + !std::is_same, detail::decay_t>::value && + !detail::conjunction, + std::is_same>>::value && + std::is_constructible::value && std::is_assignable::value>; + +template +using enable_assign_from_other = detail::enable_if_t< + std::is_constructible::value && + std::is_assignable::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_assignable &>::value && + !std::is_assignable &&>::value && + !std::is_assignable &>::value && + !std::is_assignable &&>::value>; + +// The storage base manages the actual storage, and correctly propagates +// trivial destruction from T. This case is for when T is not trivially +// destructible. +template ::value> +struct optional_storage_base { + TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept + : m_dummy(), m_has_value(false) {} + + template + TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u) + : m_value(std::forward(u)...), m_has_value(true) {} + + ~optional_storage_base() { + if (m_has_value) { + m_value.~T(); + m_has_value = false; + } + } + + struct dummy {}; + union { + dummy m_dummy; + T m_value; + }; + + bool m_has_value; +}; + +// This case is for when T is trivially destructible. +template struct optional_storage_base { + TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept + : m_dummy(), m_has_value(false) {} + + template + TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u) + : m_value(std::forward(u)...), m_has_value(true) {} + + // No destructor, so this class is trivially destructible + + struct dummy {}; + union { + dummy m_dummy; + T m_value; + }; + + bool m_has_value = false; +}; + +// This base class provides some handy member functions which can be used in +// further derived classes +template struct optional_operations_base : optional_storage_base { + using optional_storage_base::optional_storage_base; + + void hard_reset() noexcept { + get().~T(); + this->m_has_value = false; + } + + template void construct(Args &&... args) noexcept { + new (std::addressof(this->m_value)) T(std::forward(args)...); + this->m_has_value = true; + } + + template void assign(Opt &&rhs) { + if (this->has_value()) { + if (rhs.has_value()) { + this->m_value = std::forward(rhs).get(); + } else { + this->m_value.~T(); + this->m_has_value = false; + } + } + + else if (rhs.has_value()) { + construct(std::forward(rhs).get()); + } + } + + bool has_value() const { return this->m_has_value; } + + TL_OPTIONAL_11_CONSTEXPR T &get() & { return this->m_value; } + TL_OPTIONAL_11_CONSTEXPR const T &get() const & { return this->m_value; } + TL_OPTIONAL_11_CONSTEXPR T &&get() && { return std::move(this->m_value); } +#ifndef TL_OPTIONAL_NO_CONSTRR + constexpr const T &&get() const && { return std::move(this->m_value); } +#endif +}; + +// This class manages conditionally having a trivial copy constructor +// This specialization is for when T is trivially copy constructible +template +struct optional_copy_base : optional_operations_base { + using optional_operations_base::optional_operations_base; +}; + +// This specialization is for when T is not trivially copy constructible +template +struct optional_copy_base : optional_operations_base { + using optional_operations_base::optional_operations_base; + + optional_copy_base() = default; + optional_copy_base(const optional_copy_base &rhs) + : optional_operations_base() { + if (rhs.has_value()) { + this->construct(rhs.get()); + } else { + this->m_has_value = false; + } + } + + optional_copy_base(optional_copy_base &&rhs) = default; + optional_copy_base &operator=(const optional_copy_base &rhs) = default; + optional_copy_base &operator=(optional_copy_base &&rhs) = default; +}; + +// This class manages conditionally having a trivial move constructor +// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it +// doesn't implement an analogue to std::is_trivially_move_constructible. We +// have to make do with a non-trivial move constructor even if T is trivially +// move constructible +#ifndef TL_OPTIONAL_GCC49 +template ::value> +struct optional_move_base : optional_copy_base { + using optional_copy_base::optional_copy_base; +}; +#else +template struct optional_move_base; +#endif +template struct optional_move_base : optional_copy_base { + using optional_copy_base::optional_copy_base; + + optional_move_base() = default; + optional_move_base(const optional_move_base &rhs) = default; + + optional_move_base(optional_move_base &&rhs) noexcept( + std::is_nothrow_move_constructible::value) { + if (rhs.has_value()) { + this->construct(std::move(rhs.get())); + } else { + this->m_has_value = false; + } + } + optional_move_base &operator=(const optional_move_base &rhs) = default; + optional_move_base &operator=(optional_move_base &&rhs) = default; +}; + +// This class manages conditionally having a trivial copy assignment operator +template +struct optional_copy_assign_base : optional_move_base { + using optional_move_base::optional_move_base; +}; + +template +struct optional_copy_assign_base : optional_move_base { + using optional_move_base::optional_move_base; + + optional_copy_assign_base() = default; + optional_copy_assign_base(const optional_copy_assign_base &rhs) = default; + + optional_copy_assign_base(optional_copy_assign_base &&rhs) = default; + optional_copy_assign_base &operator=(const optional_copy_assign_base &rhs) { + this->assign(rhs); + return *this; + } + optional_copy_assign_base & + operator=(optional_copy_assign_base &&rhs) = default; +}; + +// This class manages conditionally having a trivial move assignment operator +// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it +// doesn't implement an analogue to std::is_trivially_move_assignable. We have +// to make do with a non-trivial move assignment operator even if T is trivially +// move assignable +#ifndef TL_OPTIONAL_GCC49 +template ::value + &&std::is_trivially_move_constructible::value + &&std::is_trivially_move_assignable::value> +struct optional_move_assign_base : optional_copy_assign_base { + using optional_copy_assign_base::optional_copy_assign_base; +}; +#else +template struct optional_move_assign_base; +#endif + +template +struct optional_move_assign_base : optional_copy_assign_base { + using optional_copy_assign_base::optional_copy_assign_base; + + optional_move_assign_base() = default; + optional_move_assign_base(const optional_move_assign_base &rhs) = default; + + optional_move_assign_base(optional_move_assign_base &&rhs) = default; + + optional_move_assign_base & + operator=(const optional_move_assign_base &rhs) = default; + + optional_move_assign_base & + operator=(optional_move_assign_base &&rhs) noexcept( + std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_assignable::value) { + this->assign(std::move(rhs)); + return *this; + } +}; + +// optional_delete_ctor_base will conditionally delete copy and move +// constructors depending on whether T is copy/move constructible +template ::value, + bool EnableMove = std::is_move_constructible::value> +struct optional_delete_ctor_base { + optional_delete_ctor_base() = default; + optional_delete_ctor_base(const optional_delete_ctor_base &) = default; + optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default; + optional_delete_ctor_base & + operator=(const optional_delete_ctor_base &) = default; + optional_delete_ctor_base & + operator=(optional_delete_ctor_base &&) noexcept = default; +}; + +template struct optional_delete_ctor_base { + optional_delete_ctor_base() = default; + optional_delete_ctor_base(const optional_delete_ctor_base &) = default; + optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete; + optional_delete_ctor_base & + operator=(const optional_delete_ctor_base &) = default; + optional_delete_ctor_base & + operator=(optional_delete_ctor_base &&) noexcept = default; +}; + +template struct optional_delete_ctor_base { + optional_delete_ctor_base() = default; + optional_delete_ctor_base(const optional_delete_ctor_base &) = delete; + optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default; + optional_delete_ctor_base & + operator=(const optional_delete_ctor_base &) = default; + optional_delete_ctor_base & + operator=(optional_delete_ctor_base &&) noexcept = default; +}; + +template struct optional_delete_ctor_base { + optional_delete_ctor_base() = default; + optional_delete_ctor_base(const optional_delete_ctor_base &) = delete; + optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete; + optional_delete_ctor_base & + operator=(const optional_delete_ctor_base &) = default; + optional_delete_ctor_base & + operator=(optional_delete_ctor_base &&) noexcept = default; +}; + +// optional_delete_assign_base will conditionally delete copy and move +// constructors depending on whether T is copy/move constructible + assignable +template ::value && + std::is_copy_assignable::value), + bool EnableMove = (std::is_move_constructible::value && + std::is_move_assignable::value)> +struct optional_delete_assign_base { + optional_delete_assign_base() = default; + optional_delete_assign_base(const optional_delete_assign_base &) = default; + optional_delete_assign_base(optional_delete_assign_base &&) noexcept = + default; + optional_delete_assign_base & + operator=(const optional_delete_assign_base &) = default; + optional_delete_assign_base & + operator=(optional_delete_assign_base &&) noexcept = default; +}; + +template struct optional_delete_assign_base { + optional_delete_assign_base() = default; + optional_delete_assign_base(const optional_delete_assign_base &) = default; + optional_delete_assign_base(optional_delete_assign_base &&) noexcept = + default; + optional_delete_assign_base & + operator=(const optional_delete_assign_base &) = default; + optional_delete_assign_base & + operator=(optional_delete_assign_base &&) noexcept = delete; +}; + +template struct optional_delete_assign_base { + optional_delete_assign_base() = default; + optional_delete_assign_base(const optional_delete_assign_base &) = default; + optional_delete_assign_base(optional_delete_assign_base &&) noexcept = + default; + optional_delete_assign_base & + operator=(const optional_delete_assign_base &) = delete; + optional_delete_assign_base & + operator=(optional_delete_assign_base &&) noexcept = default; +}; + +template struct optional_delete_assign_base { + optional_delete_assign_base() = default; + optional_delete_assign_base(const optional_delete_assign_base &) = default; + optional_delete_assign_base(optional_delete_assign_base &&) noexcept = + default; + optional_delete_assign_base & + operator=(const optional_delete_assign_base &) = delete; + optional_delete_assign_base & + operator=(optional_delete_assign_base &&) noexcept = delete; +}; + +} // namespace detail + +/// A tag type to represent an empty optional +struct nullopt_t { + struct do_not_use {}; + constexpr explicit nullopt_t(do_not_use, do_not_use) noexcept {} +}; +/// Represents an empty optional +static constexpr nullopt_t nullopt{nullopt_t::do_not_use{}, + nullopt_t::do_not_use{}}; + +class bad_optional_access : public std::exception { +public: + bad_optional_access() = default; + const char *what() const noexcept { return "Optional has no value"; } +}; + +/// An optional object is an object that contains the storage for another +/// object and manages the lifetime of this contained object, if any. The +/// contained object may be initialized after the optional object has been +/// initialized, and may be destroyed before the optional object has been +/// destroyed. The initialization state of the contained object is tracked by +/// the optional object. +template +class optional : private detail::optional_move_assign_base, + private detail::optional_delete_ctor_base, + private detail::optional_delete_assign_base { + using base = detail::optional_move_assign_base; + + static_assert(!std::is_same::value, + "instantiation of optional with in_place_t is ill-formed"); + static_assert(!std::is_same, nullopt_t>::value, + "instantiation of optional with nullopt_t is ill-formed"); + +public: +// The different versions for C++14 and 11 are needed because deduced return +// types are not SFINAE-safe. This provides better support for things like +// generic lambdas. C.f. +// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html +#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \ + !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55) + /// Carries out some operation which returns an optional on the stored + /// object if there is one. + template TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + + template TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : result(nullopt); + } + + template constexpr auto and_then(F &&f) const & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template constexpr auto and_then(F &&f) const && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : result(nullopt); + } +#endif +#else + /// Carries out some operation which returns an optional on the stored + /// object if there is one. + template + TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t and_then(F &&f) & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + + template + TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t and_then(F &&f) && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : result(nullopt); + } + + template + constexpr detail::invoke_result_t and_then(F &&f) const & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + constexpr detail::invoke_result_t and_then(F &&f) const && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : result(nullopt); + } +#endif +#endif + +#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \ + !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55) + /// Carries out some operation on the stored object if there is one. + template TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & { + return optional_map_impl(*this, std::forward(f)); + } + + template TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && { + return optional_map_impl(std::move(*this), std::forward(f)); + } + + template constexpr auto map(F &&f) const & { + return optional_map_impl(*this, std::forward(f)); + } + + template constexpr auto map(F &&f) const && { + return optional_map_impl(std::move(*this), std::forward(f)); + } +#else + /// Carries out some operation on the stored object if there is one. + template + TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval(), + std::declval())) + map(F &&f) & { + return optional_map_impl(*this, std::forward(f)); + } + + template + TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval(), + std::declval())) + map(F &&f) && { + return optional_map_impl(std::move(*this), std::forward(f)); + } + + template + constexpr decltype(optional_map_impl(std::declval(), + std::declval())) + map(F &&f) const & { + return optional_map_impl(*this, std::forward(f)); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + constexpr decltype(optional_map_impl(std::declval(), + std::declval())) + map(F &&f) const && { + return optional_map_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + +#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \ + !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55) + /// Carries out some operation on the stored object if there is one. + template TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & { + return optional_map_impl(*this, std::forward(f)); + } + + template TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && { + return optional_map_impl(std::move(*this), std::forward(f)); + } + + template constexpr auto transform(F&& f) const & { + return optional_map_impl(*this, std::forward(f)); + } + + template constexpr auto transform(F&& f) const && { + return optional_map_impl(std::move(*this), std::forward(f)); + } +#else + /// Carries out some operation on the stored object if there is one. + template + TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) & { + return optional_map_impl(*this, std::forward(f)); + } + + template + TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) && { + return optional_map_impl(std::move(*this), std::forward(f)); + } + + template + constexpr decltype(optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) const & { + return optional_map_impl(*this, std::forward(f)); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + constexpr decltype(optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) const && { + return optional_map_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + + /// Calls `f` if the optional is empty + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & { + if (has_value()) + return *this; + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & { + return has_value() ? *this : std::forward(f)(); + } + + template * = nullptr> + optional or_else(F &&f) && { + if (has_value()) + return std::move(*this); + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && { + return has_value() ? std::move(*this) : std::forward(f)(); + } + + template * = nullptr> + optional or_else(F &&f) const & { + if (has_value()) + return *this; + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & { + return has_value() ? *this : std::forward(f)(); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template * = nullptr> + optional or_else(F &&f) const && { + if (has_value()) + return std::move(*this); + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional or_else(F &&f) const && { + return has_value() ? std::move(*this) : std::forward(f)(); + } +#endif + + /// Maps the stored value with `f` if there is one, otherwise returns `u`. + template U map_or(F &&f, U &&u) & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u); + } + + template U map_or(F &&f, U &&u) && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u); + } + + template U map_or(F &&f, U &&u) const & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template U map_or(F &&f, U &&u) const && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u); + } +#endif + + /// Maps the stored value with `f` if there is one, otherwise calls + /// `u` and returns the result. + template + detail::invoke_result_t map_or_else(F &&f, U &&u) & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u)(); + } + + template + detail::invoke_result_t map_or_else(F &&f, U &&u) && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u)(); + } + + template + detail::invoke_result_t map_or_else(F &&f, U &&u) const & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u)(); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + detail::invoke_result_t map_or_else(F &&f, U &&u) const && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u)(); + } +#endif + + /// Returns `u` if `*this` has a value, otherwise an empty optional. + template + constexpr optional::type> conjunction(U &&u) const { + using result = optional>; + return has_value() ? result{u} : result{nullopt}; + } + + /// Returns `rhs` if `*this` is empty, otherwise the current value. + TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & { + return has_value() ? *this : rhs; + } + + constexpr optional disjunction(const optional &rhs) const & { + return has_value() ? *this : rhs; + } + + TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && { + return has_value() ? std::move(*this) : rhs; + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + constexpr optional disjunction(const optional &rhs) const && { + return has_value() ? std::move(*this) : rhs; + } +#endif + + TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & { + return has_value() ? *this : std::move(rhs); + } + + constexpr optional disjunction(optional &&rhs) const & { + return has_value() ? *this : std::move(rhs); + } + + TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && { + return has_value() ? std::move(*this) : std::move(rhs); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + constexpr optional disjunction(optional &&rhs) const && { + return has_value() ? std::move(*this) : std::move(rhs); + } +#endif + + /// Takes the value out of the optional, leaving it empty + optional take() { + optional ret = std::move(*this); + reset(); + return ret; + } + + using value_type = T; + + /// Constructs an optional that does not contain a value. + constexpr optional() noexcept = default; + + constexpr optional(nullopt_t) noexcept {} + + /// Copy constructor + /// + /// If `rhs` contains a value, the stored value is direct-initialized with + /// it. Otherwise, the constructed optional is empty. + TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) = default; + + /// Move constructor + /// + /// If `rhs` contains a value, the stored value is direct-initialized with + /// it. Otherwise, the constructed optional is empty. + TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default; + + /// Constructs the stored value in-place using the given arguments. + template + constexpr explicit optional( + detail::enable_if_t::value, in_place_t>, + Args &&... args) + : base(in_place, std::forward(args)...) {} + + template + TL_OPTIONAL_11_CONSTEXPR explicit optional( + detail::enable_if_t &, + Args &&...>::value, + in_place_t>, + std::initializer_list il, Args &&... args) { + this->construct(il, std::forward(args)...); + } + + /// Constructs the stored value with `u`. + template < + class U = T, + detail::enable_if_t::value> * = nullptr, + detail::enable_forward_value * = nullptr> + constexpr optional(U &&u) : base(in_place, std::forward(u)) {} + + template < + class U = T, + detail::enable_if_t::value> * = nullptr, + detail::enable_forward_value * = nullptr> + constexpr explicit optional(U &&u) : base(in_place, std::forward(u)) {} + + /// Converting copy constructor. + template < + class U, detail::enable_from_other * = nullptr, + detail::enable_if_t::value> * = nullptr> + optional(const optional &rhs) { + if (rhs.has_value()) { + this->construct(*rhs); + } + } + + template * = nullptr, + detail::enable_if_t::value> * = + nullptr> + explicit optional(const optional &rhs) { + if (rhs.has_value()) { + this->construct(*rhs); + } + } + + /// Converting move constructor. + template < + class U, detail::enable_from_other * = nullptr, + detail::enable_if_t::value> * = nullptr> + optional(optional &&rhs) { + if (rhs.has_value()) { + this->construct(std::move(*rhs)); + } + } + + template < + class U, detail::enable_from_other * = nullptr, + detail::enable_if_t::value> * = nullptr> + explicit optional(optional &&rhs) { + if (rhs.has_value()) { + this->construct(std::move(*rhs)); + } + } + + /// Destroys the stored value if there is one. + ~optional() = default; + + /// Assignment to empty. + /// + /// Destroys the current value if there is one. + optional &operator=(nullopt_t) noexcept { + if (has_value()) { + this->m_value.~T(); + this->m_has_value = false; + } + + return *this; + } + + /// Copy assignment. + /// + /// Copies the value from `rhs` if there is one. Otherwise resets the stored + /// value in `*this`. + optional &operator=(const optional &rhs) = default; + + /// Move assignment. + /// + /// Moves the value from `rhs` if there is one. Otherwise resets the stored + /// value in `*this`. + optional &operator=(optional &&rhs) = default; + + /// Assigns the stored value from `u`, destroying the old value if there was + /// one. + template * = nullptr> + optional &operator=(U &&u) { + if (has_value()) { + this->m_value = std::forward(u); + } else { + this->construct(std::forward(u)); + } + + return *this; + } + + /// Converting copy assignment operator. + /// + /// Copies the value from `rhs` if there is one. Otherwise resets the stored + /// value in `*this`. + template * = nullptr> + optional &operator=(const optional &rhs) { + if (has_value()) { + if (rhs.has_value()) { + this->m_value = *rhs; + } else { + this->hard_reset(); + } + } + + if (rhs.has_value()) { + this->construct(*rhs); + } + + return *this; + } + + // TODO check exception guarantee + /// Converting move assignment operator. + /// + /// Moves the value from `rhs` if there is one. Otherwise resets the stored + /// value in `*this`. + template * = nullptr> + optional &operator=(optional &&rhs) { + if (has_value()) { + if (rhs.has_value()) { + this->m_value = std::move(*rhs); + } else { + this->hard_reset(); + } + } + + if (rhs.has_value()) { + this->construct(std::move(*rhs)); + } + + return *this; + } + + /// Constructs the value in-place, destroying the current one if there is + /// one. + template T &emplace(Args &&... args) { + static_assert(std::is_constructible::value, + "T must be constructible with Args"); + + *this = nullopt; + this->construct(std::forward(args)...); + return value(); + } + + template + detail::enable_if_t< + std::is_constructible &, Args &&...>::value, + T &> + emplace(std::initializer_list il, Args &&... args) { + *this = nullopt; + this->construct(il, std::forward(args)...); + return value(); + } + + /// Swaps this optional with the other. + /// + /// If neither optionals have a value, nothing happens. + /// If both have a value, the values are swapped. + /// If one has a value, it is moved to the other and the movee is left + /// valueless. + void + swap(optional &rhs) noexcept(std::is_nothrow_move_constructible::value + &&detail::is_nothrow_swappable::value) { + using std::swap; + if (has_value()) { + if (rhs.has_value()) { + swap(**this, *rhs); + } else { + new (std::addressof(rhs.m_value)) T(std::move(this->m_value)); + this->m_value.T::~T(); + } + } else if (rhs.has_value()) { + new (std::addressof(this->m_value)) T(std::move(rhs.m_value)); + rhs.m_value.T::~T(); + } + swap(this->m_has_value, rhs.m_has_value); + } + + /// Returns a pointer to the stored value + constexpr const T *operator->() const { + return std::addressof(this->m_value); + } + + TL_OPTIONAL_11_CONSTEXPR T *operator->() { + return std::addressof(this->m_value); + } + + /// Returns the stored value + TL_OPTIONAL_11_CONSTEXPR T &operator*() & { return this->m_value; } + + constexpr const T &operator*() const & { return this->m_value; } + + TL_OPTIONAL_11_CONSTEXPR T &&operator*() && { + return std::move(this->m_value); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + constexpr const T &&operator*() const && { return std::move(this->m_value); } +#endif + + /// Returns whether or not the optional has a value + constexpr bool has_value() const noexcept { return this->m_has_value; } + + constexpr explicit operator bool() const noexcept { + return this->m_has_value; + } + + /// Returns the contained value if there is one, otherwise throws bad_optional_access + TL_OPTIONAL_11_CONSTEXPR T &value() & { + if (has_value()) + return this->m_value; + throw bad_optional_access(); + } + TL_OPTIONAL_11_CONSTEXPR const T &value() const & { + if (has_value()) + return this->m_value; + throw bad_optional_access(); + } + TL_OPTIONAL_11_CONSTEXPR T &&value() && { + if (has_value()) + return std::move(this->m_value); + throw bad_optional_access(); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + TL_OPTIONAL_11_CONSTEXPR const T &&value() const && { + if (has_value()) + return std::move(this->m_value); + throw bad_optional_access(); + } +#endif + + /// Returns the stored value if there is one, otherwise returns `u` + template constexpr T value_or(U &&u) const & { + static_assert(std::is_copy_constructible::value && + std::is_convertible::value, + "T must be copy constructible and convertible from U"); + return has_value() ? **this : static_cast(std::forward(u)); + } + + template TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && { + static_assert(std::is_move_constructible::value && + std::is_convertible::value, + "T must be move constructible and convertible from U"); + return has_value() ? **this : static_cast(std::forward(u)); + } + + /// Destroys the stored value if one exists, making the optional empty + void reset() noexcept { + if (has_value()) { + this->m_value.~T(); + this->m_has_value = false; + } + } +}; // namespace tl + +/// Compares two optional objects +template +inline constexpr bool operator==(const optional &lhs, + const optional &rhs) { + return lhs.has_value() == rhs.has_value() && + (!lhs.has_value() || *lhs == *rhs); +} +template +inline constexpr bool operator!=(const optional &lhs, + const optional &rhs) { + return lhs.has_value() != rhs.has_value() || + (lhs.has_value() && *lhs != *rhs); +} +template +inline constexpr bool operator<(const optional &lhs, + const optional &rhs) { + return rhs.has_value() && (!lhs.has_value() || *lhs < *rhs); +} +template +inline constexpr bool operator>(const optional &lhs, + const optional &rhs) { + return lhs.has_value() && (!rhs.has_value() || *lhs > *rhs); +} +template +inline constexpr bool operator<=(const optional &lhs, + const optional &rhs) { + return !lhs.has_value() || (rhs.has_value() && *lhs <= *rhs); +} +template +inline constexpr bool operator>=(const optional &lhs, + const optional &rhs) { + return !rhs.has_value() || (lhs.has_value() && *lhs >= *rhs); +} + +/// Compares an optional to a `nullopt` +template +inline constexpr bool operator==(const optional &lhs, nullopt_t) noexcept { + return !lhs.has_value(); +} +template +inline constexpr bool operator==(nullopt_t, const optional &rhs) noexcept { + return !rhs.has_value(); +} +template +inline constexpr bool operator!=(const optional &lhs, nullopt_t) noexcept { + return lhs.has_value(); +} +template +inline constexpr bool operator!=(nullopt_t, const optional &rhs) noexcept { + return rhs.has_value(); +} +template +inline constexpr bool operator<(const optional &, nullopt_t) noexcept { + return false; +} +template +inline constexpr bool operator<(nullopt_t, const optional &rhs) noexcept { + return rhs.has_value(); +} +template +inline constexpr bool operator<=(const optional &lhs, nullopt_t) noexcept { + return !lhs.has_value(); +} +template +inline constexpr bool operator<=(nullopt_t, const optional &) noexcept { + return true; +} +template +inline constexpr bool operator>(const optional &lhs, nullopt_t) noexcept { + return lhs.has_value(); +} +template +inline constexpr bool operator>(nullopt_t, const optional &) noexcept { + return false; +} +template +inline constexpr bool operator>=(const optional &, nullopt_t) noexcept { + return true; +} +template +inline constexpr bool operator>=(nullopt_t, const optional &rhs) noexcept { + return !rhs.has_value(); +} + +/// Compares the optional with a value. +template +inline constexpr bool operator==(const optional &lhs, const U &rhs) { + return lhs.has_value() ? *lhs == rhs : false; +} +template +inline constexpr bool operator==(const U &lhs, const optional &rhs) { + return rhs.has_value() ? lhs == *rhs : false; +} +template +inline constexpr bool operator!=(const optional &lhs, const U &rhs) { + return lhs.has_value() ? *lhs != rhs : true; +} +template +inline constexpr bool operator!=(const U &lhs, const optional &rhs) { + return rhs.has_value() ? lhs != *rhs : true; +} +template +inline constexpr bool operator<(const optional &lhs, const U &rhs) { + return lhs.has_value() ? *lhs < rhs : true; +} +template +inline constexpr bool operator<(const U &lhs, const optional &rhs) { + return rhs.has_value() ? lhs < *rhs : false; +} +template +inline constexpr bool operator<=(const optional &lhs, const U &rhs) { + return lhs.has_value() ? *lhs <= rhs : true; +} +template +inline constexpr bool operator<=(const U &lhs, const optional &rhs) { + return rhs.has_value() ? lhs <= *rhs : false; +} +template +inline constexpr bool operator>(const optional &lhs, const U &rhs) { + return lhs.has_value() ? *lhs > rhs : false; +} +template +inline constexpr bool operator>(const U &lhs, const optional &rhs) { + return rhs.has_value() ? lhs > *rhs : true; +} +template +inline constexpr bool operator>=(const optional &lhs, const U &rhs) { + return lhs.has_value() ? *lhs >= rhs : false; +} +template +inline constexpr bool operator>=(const U &lhs, const optional &rhs) { + return rhs.has_value() ? lhs >= *rhs : true; +} + +template ::value> * = nullptr, + detail::enable_if_t::value> * = nullptr> +void swap(optional &lhs, + optional &rhs) noexcept(noexcept(lhs.swap(rhs))) { + return lhs.swap(rhs); +} + +namespace detail { +struct i_am_secret {}; +} // namespace detail + +template ::value, + detail::decay_t, T>> +inline constexpr optional make_optional(U &&v) { + return optional(std::forward(v)); +} + +template +inline constexpr optional make_optional(Args &&... args) { + return optional(in_place, std::forward(args)...); +} +template +inline constexpr optional make_optional(std::initializer_list il, + Args &&... args) { + return optional(in_place, il, std::forward(args)...); +} + +#if __cplusplus >= 201703L +template optional(T)->optional; +#endif + +/// \exclude +namespace detail { +#ifdef TL_OPTIONAL_CXX14 +template (), + *std::declval())), + detail::enable_if_t::value> * = nullptr> +constexpr auto optional_map_impl(Opt &&opt, F &&f) { + return opt.has_value() + ? detail::invoke(std::forward(f), *std::forward(opt)) + : optional(nullopt); +} + +template (), + *std::declval())), + detail::enable_if_t::value> * = nullptr> +auto optional_map_impl(Opt &&opt, F &&f) { + if (opt.has_value()) { + detail::invoke(std::forward(f), *std::forward(opt)); + return make_optional(monostate{}); + } + + return optional(nullopt); +} +#else +template (), + *std::declval())), + detail::enable_if_t::value> * = nullptr> + +constexpr auto optional_map_impl(Opt &&opt, F &&f) -> optional { + return opt.has_value() + ? detail::invoke(std::forward(f), *std::forward(opt)) + : optional(nullopt); +} + +template (), + *std::declval())), + detail::enable_if_t::value> * = nullptr> + +auto optional_map_impl(Opt &&opt, F &&f) -> optional { + if (opt.has_value()) { + detail::invoke(std::forward(f), *std::forward(opt)); + return monostate{}; + } + + return nullopt; +} +#endif +} // namespace detail + +/// Specialization for when `T` is a reference. `optional` acts similarly +/// to a `T*`, but provides more operations and shows intent more clearly. +template class optional { +public: +// The different versions for C++14 and 11 are needed because deduced return +// types are not SFINAE-safe. This provides better support for things like +// generic lambdas. C.f. +// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html +#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \ + !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55) + + /// Carries out some operation which returns an optional on the stored + /// object if there is one. + template TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + + template TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + + template constexpr auto and_then(F &&f) const & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template constexpr auto and_then(F &&f) const && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } +#endif +#else + /// Carries out some operation which returns an optional on the stored + /// object if there is one. + template + TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t and_then(F &&f) & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + + template + TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t and_then(F &&f) && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + + template + constexpr detail::invoke_result_t and_then(F &&f) const & { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + constexpr detail::invoke_result_t and_then(F &&f) const && { + using result = detail::invoke_result_t; + static_assert(detail::is_optional::value, + "F must return an optional"); + + return has_value() ? detail::invoke(std::forward(f), **this) + : result(nullopt); + } +#endif +#endif + +#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \ + !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55) + /// Carries out some operation on the stored object if there is one. + template TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & { + return detail::optional_map_impl(*this, std::forward(f)); + } + + template TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } + + template constexpr auto map(F &&f) const & { + return detail::optional_map_impl(*this, std::forward(f)); + } + + template constexpr auto map(F &&f) const && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } +#else + /// Carries out some operation on the stored object if there is one. + template + TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval(), + std::declval())) + map(F &&f) & { + return detail::optional_map_impl(*this, std::forward(f)); + } + + template + TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval(), + std::declval())) + map(F &&f) && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } + + template + constexpr decltype(detail::optional_map_impl(std::declval(), + std::declval())) + map(F &&f) const & { + return detail::optional_map_impl(*this, std::forward(f)); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + constexpr decltype(detail::optional_map_impl(std::declval(), + std::declval())) + map(F &&f) const && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + +#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \ + !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55) + /// Carries out some operation on the stored object if there is one. + template TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & { + return detail::optional_map_impl(*this, std::forward(f)); + } + + template TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } + + template constexpr auto transform(F&& f) const & { + return detail::optional_map_impl(*this, std::forward(f)); + } + + template constexpr auto transform(F&& f) const && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } +#else + /// Carries out some operation on the stored object if there is one. + template + TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) & { + return detail::optional_map_impl(*this, std::forward(f)); + } + + /// \group map + /// \synopsis template auto transform(F &&f) &&; + template + TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } + + template + constexpr decltype(detail::optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) const & { + return detail::optional_map_impl(*this, std::forward(f)); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + constexpr decltype(detail::optional_map_impl(std::declval(), + std::declval())) + transform(F&& f) const && { + return detail::optional_map_impl(std::move(*this), std::forward(f)); + } +#endif +#endif + + /// Calls `f` if the optional is empty + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & { + if (has_value()) + return *this; + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & { + return has_value() ? *this : std::forward(f)(); + } + + template * = nullptr> + optional or_else(F &&f) && { + if (has_value()) + return std::move(*this); + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && { + return has_value() ? std::move(*this) : std::forward(f)(); + } + + template * = nullptr> + optional or_else(F &&f) const & { + if (has_value()) + return *this; + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & { + return has_value() ? *this : std::forward(f)(); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template * = nullptr> + optional or_else(F &&f) const && { + if (has_value()) + return std::move(*this); + + std::forward(f)(); + return nullopt; + } + + template * = nullptr> + optional or_else(F &&f) const && { + return has_value() ? std::move(*this) : std::forward(f)(); + } +#endif + + /// Maps the stored value with `f` if there is one, otherwise returns `u` + template U map_or(F &&f, U &&u) & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u); + } + + template U map_or(F &&f, U &&u) && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u); + } + + template U map_or(F &&f, U &&u) const & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template U map_or(F &&f, U &&u) const && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u); + } +#endif + + /// Maps the stored value with `f` if there is one, otherwise calls + /// `u` and returns the result. + template + detail::invoke_result_t map_or_else(F &&f, U &&u) & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u)(); + } + + template + detail::invoke_result_t map_or_else(F &&f, U &&u) && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u)(); + } + + template + detail::invoke_result_t map_or_else(F &&f, U &&u) const & { + return has_value() ? detail::invoke(std::forward(f), **this) + : std::forward(u)(); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + template + detail::invoke_result_t map_or_else(F &&f, U &&u) const && { + return has_value() ? detail::invoke(std::forward(f), std::move(**this)) + : std::forward(u)(); + } +#endif + + /// Returns `u` if `*this` has a value, otherwise an empty optional. + template + constexpr optional::type> conjunction(U &&u) const { + using result = optional>; + return has_value() ? result{u} : result{nullopt}; + } + + /// Returns `rhs` if `*this` is empty, otherwise the current value. + TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & { + return has_value() ? *this : rhs; + } + + constexpr optional disjunction(const optional &rhs) const & { + return has_value() ? *this : rhs; + } + + TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && { + return has_value() ? std::move(*this) : rhs; + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + constexpr optional disjunction(const optional &rhs) const && { + return has_value() ? std::move(*this) : rhs; + } +#endif + + TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & { + return has_value() ? *this : std::move(rhs); + } + + constexpr optional disjunction(optional &&rhs) const & { + return has_value() ? *this : std::move(rhs); + } + + TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && { + return has_value() ? std::move(*this) : std::move(rhs); + } + +#ifndef TL_OPTIONAL_NO_CONSTRR + constexpr optional disjunction(optional &&rhs) const && { + return has_value() ? std::move(*this) : std::move(rhs); + } +#endif + + /// Takes the value out of the optional, leaving it empty + optional take() { + optional ret = std::move(*this); + reset(); + return ret; + } + + using value_type = T &; + + /// Constructs an optional that does not contain a value. + constexpr optional() noexcept : m_value(nullptr) {} + + constexpr optional(nullopt_t) noexcept : m_value(nullptr) {} + + /// Copy constructor + /// + /// If `rhs` contains a value, the stored value is direct-initialized with + /// it. Otherwise, the constructed optional is empty. + TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) noexcept = default; + + /// Move constructor + /// + /// If `rhs` contains a value, the stored value is direct-initialized with + /// it. Otherwise, the constructed optional is empty. + TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default; + + /// Constructs the stored value with `u`. + template >::value> + * = nullptr> + constexpr optional(U &&u) noexcept : m_value(std::addressof(u)) { + static_assert(std::is_lvalue_reference::value, "U must be an lvalue"); + } + + template + constexpr explicit optional(const optional &rhs) noexcept : optional(*rhs) {} + + /// No-op + ~optional() = default; + + /// Assignment to empty. + /// + /// Destroys the current value if there is one. + optional &operator=(nullopt_t) noexcept { + m_value = nullptr; + return *this; + } + + /// Copy assignment. + /// + /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise + /// resets the stored value in `*this`. + optional &operator=(const optional &rhs) = default; + + /// Rebinds this optional to `u`. + template >::value> + * = nullptr> + optional &operator=(U &&u) { + static_assert(std::is_lvalue_reference::value, "U must be an lvalue"); + m_value = std::addressof(u); + return *this; + } + + /// Converting copy assignment operator. + /// + /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise + /// resets the stored value in `*this`. + template optional &operator=(const optional &rhs) noexcept { + m_value = std::addressof(rhs.value()); + return *this; + } + + /// Rebinds this optional to `u`. + template >::value> + * = nullptr> + optional &emplace(U &&u) noexcept { + return *this = std::forward(u); + } + + void swap(optional &rhs) noexcept { std::swap(m_value, rhs.m_value); } + + /// Returns a pointer to the stored value + constexpr const T *operator->() const noexcept { return m_value; } + + TL_OPTIONAL_11_CONSTEXPR T *operator->() noexcept { return m_value; } + + /// Returns the stored value + TL_OPTIONAL_11_CONSTEXPR T &operator*() noexcept { return *m_value; } + + constexpr const T &operator*() const noexcept { return *m_value; } + + constexpr bool has_value() const noexcept { return m_value != nullptr; } + + constexpr explicit operator bool() const noexcept { + return m_value != nullptr; + } + + /// Returns the contained value if there is one, otherwise throws bad_optional_access + TL_OPTIONAL_11_CONSTEXPR T &value() { + if (has_value()) + return *m_value; + throw bad_optional_access(); + } + TL_OPTIONAL_11_CONSTEXPR const T &value() const { + if (has_value()) + return *m_value; + throw bad_optional_access(); + } + + /// Returns the stored value if there is one, otherwise returns `u` + template constexpr T value_or(U &&u) const & noexcept { + static_assert(std::is_copy_constructible::value && + std::is_convertible::value, + "T must be copy constructible and convertible from U"); + return has_value() ? **this : static_cast(std::forward(u)); + } + + /// \group value_or + template TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && noexcept { + static_assert(std::is_move_constructible::value && + std::is_convertible::value, + "T must be move constructible and convertible from U"); + return has_value() ? **this : static_cast(std::forward(u)); + } + + /// Destroys the stored value if one exists, making the optional empty + void reset() noexcept { m_value = nullptr; } + +private: + T *m_value; +}; // namespace tl + + + +} // namespace tl + +namespace std { +// TODO SFINAE +template struct hash> { + ::std::size_t operator()(const tl::optional &o) const { + if (!o.has_value()) + return 0; + + return std::hash>()(*o); + } +}; +} // namespace std + +#endif diff --git a/lib/thirdparty/tabulate.hpp b/lib/thirdparty/tabulate.hpp new file mode 100644 index 0000000..ef217aa --- /dev/null +++ b/lib/thirdparty/tabulate.hpp @@ -0,0 +1,9235 @@ +// Copyright 2016-2018 by Martin Moene +// +// https://github.com/martinmoene/variant-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_VARIANT_LITE_HPP +#define NONSTD_VARIANT_LITE_HPP + +#define variant_lite_MAJOR 1 +#define variant_lite_MINOR 2 +#define variant_lite_PATCH 2 + +#define variant_lite_VERSION \ + variant_STRINGIFY(variant_lite_MAJOR) "." variant_STRINGIFY( \ + variant_lite_MINOR) "." variant_STRINGIFY(variant_lite_PATCH) + +#define variant_STRINGIFY(x) variant_STRINGIFY_(x) +#define variant_STRINGIFY_(x) #x + +// variant-lite configuration: + +#define variant_VARIANT_DEFAULT 0 +#define variant_VARIANT_NONSTD 1 +#define variant_VARIANT_STD 2 + +#if !defined(variant_CONFIG_SELECT_VARIANT) +#define variant_CONFIG_SELECT_VARIANT \ + (variant_HAVE_STD_VARIANT ? variant_VARIANT_STD : variant_VARIANT_NONSTD) +#endif + +#ifndef variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO +#define variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO 0 +#endif + +#ifndef variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO +#define variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO 0 +#endif + +// Control presence of exception handling (try and auto discover): + +#ifndef variant_CONFIG_NO_EXCEPTIONS +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) +#define variant_CONFIG_NO_EXCEPTIONS 0 +#else +#define variant_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 variant_CPLUSPLUS +#if defined(_MSVC_LANG) && !defined(__clang__) +#define variant_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG) +#else +#define variant_CPLUSPLUS __cplusplus +#endif +#endif + +#define variant_CPP98_OR_GREATER (variant_CPLUSPLUS >= 199711L) +#define variant_CPP11_OR_GREATER (variant_CPLUSPLUS >= 201103L) +#define variant_CPP11_OR_GREATER_ (variant_CPLUSPLUS >= 201103L) +#define variant_CPP14_OR_GREATER (variant_CPLUSPLUS >= 201402L) +#define variant_CPP17_OR_GREATER (variant_CPLUSPLUS >= 201703L) +#define variant_CPP20_OR_GREATER (variant_CPLUSPLUS >= 202000L) + +// Use C++17 std::variant if available and requested: + +#if variant_CPP17_OR_GREATER && defined(__has_include) +#if __has_include( ) +#define variant_HAVE_STD_VARIANT 1 +#else +#define variant_HAVE_STD_VARIANT 0 +#endif +#else +#define variant_HAVE_STD_VARIANT 0 +#endif + +#define variant_USES_STD_VARIANT \ + ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_STD) || \ + ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_DEFAULT) && variant_HAVE_STD_VARIANT)) + +// +// 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 : + +#if variant_CPP17_OR_GREATER + +#include + +namespace nonstd { + +using std::in_place; +using std::in_place_index; +using std::in_place_index_t; +using std::in_place_t; +using std::in_place_type; +using std::in_place_type_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 +#define nonstd_lite_in_place_index_t(K) std::in_place_index_t + +#define nonstd_lite_in_place(T) \ + std::in_place_t {} +#define nonstd_lite_in_place_type(T) \ + std::in_place_type_t {} +#define nonstd_lite_in_place_index(K) \ + std::in_place_index_t {} + +} // namespace nonstd + +#else // variant_CPP17_OR_GREATER + +#include + +namespace nonstd { +namespace detail { + +template struct in_place_type_tag {}; + +template struct in_place_index_tag {}; + +} // namespace detail + +struct in_place_t {}; + +template +inline in_place_t in_place(detail::in_place_type_tag = detail::in_place_type_tag()) { + return in_place_t(); +} + +template +inline in_place_t in_place(detail::in_place_index_tag = detail::in_place_index_tag()) { + return in_place_t(); +} + +template +inline in_place_t in_place_type(detail::in_place_type_tag = detail::in_place_type_tag()) { + return in_place_t(); +} + +template +inline in_place_t in_place_index(detail::in_place_index_tag = detail::in_place_index_tag()) { + return in_place_t(); +} + +// mimic templated typedef: + +#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag) +#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag) +#define nonstd_lite_in_place_index_t(K) \ + nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag) + +#define nonstd_lite_in_place(T) nonstd::in_place_type +#define nonstd_lite_in_place_type(T) nonstd::in_place_type +#define nonstd_lite_in_place_index(K) nonstd::in_place_index + +} // namespace nonstd + +#endif // variant_CPP17_OR_GREATER +#endif // nonstd_lite_HAVE_IN_PLACE_TYPES + +// +// Use C++17 std::variant: +// + +#if variant_USES_STD_VARIANT + +#include // std::hash<> +#include + +#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO +#define variant_size_V(T) nonstd::variant_size::value +#endif + +#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO +#define variant_alternative_T(K, T) typename nonstd::variant_alternative::type +#endif + +namespace nonstd { + +using std::bad_variant_access; +using std::hash; +using std::monostate; +using std::variant; +using std::variant_alternative; +using std::variant_alternative_t; +using std::variant_size; +using std::variant_size_v; + +using std::get; +using std::get_if; +using std::holds_alternative; +using std::visit; +using std::operator==; +using std::operator!=; +using std::operator<; +using std::operator<=; +using std::operator>; +using std::operator>=; +using std::swap; + +constexpr auto variant_npos = std::variant_npos; +} // namespace nonstd + +#else // variant_USES_STD_VARIANT + +#include +#include +#include +#include + +#if variant_CONFIG_NO_EXCEPTIONS +#include +#else +#include +#endif + +// variant-lite type and visitor argument count configuration (script/generate_header.py): + +#define variant_CONFIG_MAX_TYPE_COUNT 16 +#define variant_CONFIG_MAX_VISITOR_ARG_COUNT 5 + +// variant-lite alignment configuration: + +#ifndef variant_CONFIG_MAX_ALIGN_HACK +#define variant_CONFIG_MAX_ALIGN_HACK 0 +#endif + +#ifndef variant_CONFIG_ALIGN_AS +// no default, used in #if defined() +#endif + +#ifndef variant_CONFIG_ALIGN_AS_FALLBACK +#define variant_CONFIG_ALIGN_AS_FALLBACK double +#endif + +// half-open range [lo..hi): +#define variant_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi)) + +// Compiler versions: +// +// MSVC++ 6.0 _MSC_VER == 1200 variant_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0) +// MSVC++ 7.0 _MSC_VER == 1300 variant_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002) +// MSVC++ 7.1 _MSC_VER == 1310 variant_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003) +// MSVC++ 8.0 _MSC_VER == 1400 variant_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005) +// MSVC++ 9.0 _MSC_VER == 1500 variant_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008) +// MSVC++ 10.0 _MSC_VER == 1600 variant_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010) +// MSVC++ 11.0 _MSC_VER == 1700 variant_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012) +// MSVC++ 12.0 _MSC_VER == 1800 variant_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013) +// MSVC++ 14.0 _MSC_VER == 1900 variant_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015) +// MSVC++ 14.1 _MSC_VER >= 1910 variant_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017) +// MSVC++ 14.2 _MSC_VER >= 1920 variant_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019) + +#if defined(_MSC_VER) && !defined(__clang__) +#define variant_COMPILER_MSVC_VER (_MSC_VER) +#define variant_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900))) +#else +#define variant_COMPILER_MSVC_VER 0 +#define variant_COMPILER_MSVC_VERSION 0 +#endif + +#define variant_COMPILER_VERSION(major, minor, patch) (10 * (10 * (major) + (minor)) + (patch)) + +#if defined(__clang__) +#define variant_COMPILER_CLANG_VERSION \ + variant_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +#else +#define variant_COMPILER_CLANG_VERSION 0 +#endif + +#if defined(__GNUC__) && !defined(__clang__) +#define variant_COMPILER_GNUC_VERSION \ + variant_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#else +#define variant_COMPILER_GNUC_VERSION 0 +#endif + +#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900) +#pragma warning(push) +#pragma warning(disable : 4345) // initialization behavior changed +#endif + +// Presence of language and library features: + +#define variant_HAVE(feature) (variant_HAVE_##feature) + +#ifdef _HAS_CPP0X +#define variant_HAS_CPP0X _HAS_CPP0X +#else +#define variant_HAS_CPP0X 0 +#endif + +// Unless defined otherwise below, consider VC14 as C++11 for variant-lite: + +#if variant_COMPILER_MSVC_VER >= 1900 +#undef variant_CPP11_OR_GREATER +#define variant_CPP11_OR_GREATER 1 +#endif + +#define variant_CPP11_90 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1500) +#define variant_CPP11_100 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1600) +#define variant_CPP11_110 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1700) +#define variant_CPP11_120 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1800) +#define variant_CPP11_140 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1900) +#define variant_CPP11_141 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1910) + +#define variant_CPP14_000 (variant_CPP14_OR_GREATER) +#define variant_CPP17_000 (variant_CPP17_OR_GREATER) + +// Presence of C++11 language features: + +#define variant_HAVE_CONSTEXPR_11 variant_CPP11_140 +#define variant_HAVE_INITIALIZER_LIST variant_CPP11_120 +#define variant_HAVE_NOEXCEPT variant_CPP11_140 +#define variant_HAVE_NULLPTR variant_CPP11_100 +#define variant_HAVE_OVERRIDE variant_CPP11_140 + +// Presence of C++14 language features: + +#define variant_HAVE_CONSTEXPR_14 variant_CPP14_000 + +// Presence of C++17 language features: + +// no flag + +// Presence of C++ library features: + +#define variant_HAVE_CONDITIONAL variant_CPP11_120 +#define variant_HAVE_REMOVE_CV variant_CPP11_120 +#define variant_HAVE_STD_ADD_POINTER variant_CPP11_90 +#define variant_HAVE_TYPE_TRAITS variant_CPP11_90 + +#define variant_HAVE_TR1_TYPE_TRAITS (!!variant_COMPILER_GNUC_VERSION) +#define variant_HAVE_TR1_ADD_POINTER (!!variant_COMPILER_GNUC_VERSION) + +// C++ feature usage: + +#if variant_HAVE_CONSTEXPR_11 +#define variant_constexpr constexpr +#else +#define variant_constexpr /*constexpr*/ +#endif + +#if variant_HAVE_CONSTEXPR_14 +#define variant_constexpr14 constexpr +#else +#define variant_constexpr14 /*constexpr*/ +#endif + +#if variant_HAVE_NOEXCEPT +#define variant_noexcept noexcept +#else +#define variant_noexcept /*noexcept*/ +#endif + +#if variant_HAVE_NULLPTR +#define variant_nullptr nullptr +#else +#define variant_nullptr NULL +#endif + +#if variant_HAVE_OVERRIDE +#define variant_override override +#else +#define variant_override /*override*/ +#endif + +// additional includes: + +#if variant_CPP11_OR_GREATER +#include // std::hash +#endif + +#if variant_HAVE_INITIALIZER_LIST +#include +#endif + +#if variant_HAVE_TYPE_TRAITS +#include +#elif variant_HAVE_TR1_TYPE_TRAITS +#include +#endif + +// Method enabling + +#if variant_CPP11_OR_GREATER + +#define variant_REQUIRES_0(...) \ + template ::type = 0> + +#define variant_REQUIRES_T(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0 + +#define variant_REQUIRES_R(R, ...) typename std::enable_if<(__VA_ARGS__), R>::type + +#define variant_REQUIRES_A(...) , typename std::enable_if<(__VA_ARGS__), void *>::type = nullptr + +#endif + +// +// variant: +// + +namespace nonstd { +namespace variants { + +// C++11 emulation: + +namespace std11 { + +#if variant_HAVE_STD_ADD_POINTER + +using std::add_pointer; + +#elif variant_HAVE_TR1_ADD_POINTER + +using std::tr1::add_pointer; + +#else + +template struct remove_reference { typedef T type; }; +template struct remove_reference { typedef T type; }; + +template struct add_pointer { typedef typename remove_reference::type *type; }; + +#endif // variant_HAVE_STD_ADD_POINTER + +#if variant_HAVE_REMOVE_CV + +using std::remove_cv; + +#else + +template struct remove_const { typedef T type; }; +template struct remove_const { typedef T type; }; + +template struct remove_volatile { typedef T type; }; +template struct remove_volatile { typedef T type; }; + +template struct remove_cv { + typedef typename remove_volatile::type>::type type; +}; + +#endif // variant_HAVE_REMOVE_CV + +#if variant_HAVE_CONDITIONAL + +using std::conditional; + +#else + +template struct conditional; + +template struct conditional { typedef Then type; }; + +template struct conditional { typedef Else type; }; + +#endif // variant_HAVE_CONDITIONAL + +} // namespace std11 + +/// type traits C++17: + +namespace std17 { + +#if variant_CPP17_OR_GREATER + +using std::is_nothrow_swappable; +using std::is_swappable; + +#elif variant_CPP11_OR_GREATER + +namespace detail { + +using std::swap; + +struct is_swappable { + template (), std::declval()))> + static std::true_type test(int); + + template static std::false_type test(...); +}; + +struct is_nothrow_swappable { + // wrap noexcept(epr) in separate function as work-around for VC140 (VS2015): + + template static constexpr bool test() { + return noexcept(swap(std::declval(), std::declval())); + } + + template static auto test(int) -> std::integral_constant()> {} + + template static std::false_type test(...); +}; + +} // namespace detail + +// is [nothow] swappable: + +template struct is_swappable : decltype(detail::is_swappable::test(0)) {}; + +template +struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test(0)) {}; + +#endif // variant_CPP17_OR_GREATER + +} // namespace std17 + +// detail: + +namespace detail { + +// typelist: + +#define variant_TL1(T1) detail::typelist +#define variant_TL2(T1, T2) detail::typelist +#define variant_TL3(T1, T2, T3) detail::typelist +#define variant_TL4(T1, T2, T3, T4) detail::typelist +#define variant_TL5(T1, T2, T3, T4, T5) detail::typelist +#define variant_TL6(T1, T2, T3, T4, T5, T6) detail::typelist +#define variant_TL7(T1, T2, T3, T4, T5, T6, T7) \ + detail::typelist +#define variant_TL8(T1, T2, T3, T4, T5, T6, T7, T8) \ + detail::typelist +#define variant_TL9(T1, T2, T3, T4, T5, T6, T7, T8, T9) \ + detail::typelist +#define variant_TL10(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) \ + detail::typelist +#define variant_TL11(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) \ + detail::typelist +#define variant_TL12(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) \ + detail::typelist +#define variant_TL13(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) \ + detail::typelist +#define variant_TL14(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) \ + detail::typelist +#define variant_TL15(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) \ + detail::typelist +#define variant_TL16(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) \ + detail::typelist + +// variant parameter unused type tags: + +template struct TX : T { + inline TX operator+() const { return TX(); } + inline TX operator-() const { return TX(); } + + inline TX operator!() const { return TX(); } + inline TX operator~() const { return TX(); } + + inline TX *operator&() const { return variant_nullptr; } + + template inline TX operator*(U const &)const { return TX(); } + template inline TX operator/(U const &) const { return TX(); } + + template inline TX operator%(U const &) const { return TX(); } + template inline TX operator+(U const &) const { return TX(); } + template inline TX operator-(U const &) const { return TX(); } + + template inline TX operator<<(U const &) const { return TX(); } + template inline TX operator>>(U const &) const { return TX(); } + + inline bool operator==(T const &) const { return false; } + inline bool operator<(T const &) const { return false; } + + template inline TX operator&(U const &)const { return TX(); } + template inline TX operator|(U const &) const { return TX(); } + template inline TX operator^(U const &) const { return TX(); } + + template inline TX operator&&(U const &) const { return TX(); } + template inline TX operator||(U const &) const { return TX(); } +}; + +struct S0 {}; +typedef TX T0; +struct S1 {}; +typedef TX T1; +struct S2 {}; +typedef TX T2; +struct S3 {}; +typedef TX T3; +struct S4 {}; +typedef TX T4; +struct S5 {}; +typedef TX T5; +struct S6 {}; +typedef TX T6; +struct S7 {}; +typedef TX T7; +struct S8 {}; +typedef TX T8; +struct S9 {}; +typedef TX T9; +struct S10 {}; +typedef TX T10; +struct S11 {}; +typedef TX T11; +struct S12 {}; +typedef TX T12; +struct S13 {}; +typedef TX T13; +struct S14 {}; +typedef TX T14; +struct S15 {}; +typedef TX T15; + +struct nulltype {}; + +template struct typelist { + typedef Head head; + typedef Tail tail; +}; + +// typelist max element size: + +template struct typelist_max; + +template <> struct typelist_max { + enum V { value = 0 }; + typedef void type; +}; + +template struct typelist_max> { +private: + enum TV { tail_value = size_t(typelist_max::value) }; + + typedef typename typelist_max::type tail_type; + +public: + enum V { value = (sizeof(Head) > tail_value) ? sizeof(Head) : std::size_t(tail_value) }; + + typedef typename std11::conditional<(sizeof(Head) > tail_value), Head, tail_type>::type type; +}; + +#if variant_CPP11_OR_GREATER + +// typelist max alignof element type: + +template struct typelist_max_alignof; + +template <> struct typelist_max_alignof { + enum V { value = 0 }; +}; + +template struct typelist_max_alignof> { +private: + enum TV { tail_value = size_t(typelist_max_alignof::value) }; + +public: + enum V { value = (alignof(Head) > tail_value) ? alignof(Head) : std::size_t(tail_value) }; +}; + +#endif + +// typelist size (length): + +template struct typelist_size { + enum V { value = 1 }; +}; + +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; +template <> struct typelist_size { + enum V { value = 0 }; +}; + +template <> struct typelist_size { + enum V { value = 0 }; +}; + +template struct typelist_size> { + enum V { value = typelist_size::value + typelist_size::value }; +}; + +// typelist index of type: + +template struct typelist_index_of; + +template struct typelist_index_of { + enum V { value = -1 }; +}; + +template struct typelist_index_of, T> { + enum V { value = 0 }; +}; + +template struct typelist_index_of, T> { +private: + enum TV { nextVal = typelist_index_of::value }; + +public: + enum V { value = nextVal == -1 ? -1 : 1 + nextVal }; +}; + +// typelist type at index: + +template struct typelist_type_at; + +template struct typelist_type_at, 0> { + typedef Head type; +}; + +template struct typelist_type_at, i> { + typedef typename typelist_type_at::type type; +}; + +#if variant_CONFIG_MAX_ALIGN_HACK + +// Max align, use most restricted type for alignment: + +#define variant_UNIQUE(name) variant_UNIQUE2(name, __LINE__) +#define variant_UNIQUE2(name, line) variant_UNIQUE3(name, line) +#define variant_UNIQUE3(name, line) name##line + +#define variant_ALIGN_TYPE(type) \ + type variant_UNIQUE(_t); \ + struct_t variant_UNIQUE(_st) + +template struct struct_t { T _; }; + +union max_align_t { + variant_ALIGN_TYPE(char); + variant_ALIGN_TYPE(short int); + variant_ALIGN_TYPE(int); + variant_ALIGN_TYPE(long int); + variant_ALIGN_TYPE(float); + variant_ALIGN_TYPE(double); + variant_ALIGN_TYPE(long double); + variant_ALIGN_TYPE(char *); + variant_ALIGN_TYPE(short int *); + variant_ALIGN_TYPE(int *); + variant_ALIGN_TYPE(long int *); + variant_ALIGN_TYPE(float *); + variant_ALIGN_TYPE(double *); + variant_ALIGN_TYPE(long double *); + variant_ALIGN_TYPE(void *); + +#ifdef HAVE_LONG_LONG + variant_ALIGN_TYPE(long long); +#endif + + struct Unknown; + + Unknown (*variant_UNIQUE(_))(Unknown); + Unknown *Unknown::*variant_UNIQUE(_); + Unknown (Unknown::*variant_UNIQUE(_))(Unknown); + + struct_t variant_UNIQUE(_); + struct_t variant_UNIQUE(_); + struct_t variant_UNIQUE(_); +}; + +#undef variant_UNIQUE +#undef variant_UNIQUE2 +#undef variant_UNIQUE3 + +#undef variant_ALIGN_TYPE + +#elif defined(variant_CONFIG_ALIGN_AS) // variant_CONFIG_MAX_ALIGN_HACK + +// Use user-specified type for alignment: + +#define variant_ALIGN_AS(unused) variant_CONFIG_ALIGN_AS + +#else // variant_CONFIG_MAX_ALIGN_HACK + +// Determine POD type to use for alignment: + +#define variant_ALIGN_AS(to_align) \ + typename detail::type_of_size::value>::type + +template struct alignment_of; + +template struct alignment_of_hack { + char c; + T t; + alignment_of_hack(); +}; + +template struct alignment_logic { + enum V { value = A < S ? A : S }; +}; + +template struct alignment_of { + enum V { value = alignment_logic) - sizeof(T), sizeof(T)>::value }; +}; + +template struct type_of_size { + typedef + typename std11::conditional::type>::type type; +}; + +template struct type_of_size { + typedef variant_CONFIG_ALIGN_AS_FALLBACK type; +}; + +template struct struct_t { T _; }; + +#define variant_ALIGN_TYPE(type) typelist < type, typelist < struct_t + +struct Unknown; + +typedef variant_ALIGN_TYPE(char), variant_ALIGN_TYPE(short), variant_ALIGN_TYPE(int), + variant_ALIGN_TYPE(long), variant_ALIGN_TYPE(float), variant_ALIGN_TYPE(double), + variant_ALIGN_TYPE(long double), + + variant_ALIGN_TYPE(char *), variant_ALIGN_TYPE(short *), variant_ALIGN_TYPE(int *), + variant_ALIGN_TYPE(long *), variant_ALIGN_TYPE(float *), variant_ALIGN_TYPE(double *), + variant_ALIGN_TYPE(long double *), + + variant_ALIGN_TYPE(Unknown (*)(Unknown)), variant_ALIGN_TYPE(Unknown *Unknown::*), + variant_ALIGN_TYPE(Unknown (Unknown::*)(Unknown)), + + nulltype >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> alignment_types; + +#undef variant_ALIGN_TYPE + +#endif // variant_CONFIG_MAX_ALIGN_HACK + +#if variant_CPP11_OR_GREATER + +template inline std::size_t hash(T const &v) { return std::hash()(v); } + +inline std::size_t hash(T0 const &) { return 0; } +inline std::size_t hash(T1 const &) { return 0; } +inline std::size_t hash(T2 const &) { return 0; } +inline std::size_t hash(T3 const &) { return 0; } +inline std::size_t hash(T4 const &) { return 0; } +inline std::size_t hash(T5 const &) { return 0; } +inline std::size_t hash(T6 const &) { return 0; } +inline std::size_t hash(T7 const &) { return 0; } +inline std::size_t hash(T8 const &) { return 0; } +inline std::size_t hash(T9 const &) { return 0; } +inline std::size_t hash(T10 const &) { return 0; } +inline std::size_t hash(T11 const &) { return 0; } +inline std::size_t hash(T12 const &) { return 0; } +inline std::size_t hash(T13 const &) { return 0; } +inline std::size_t hash(T14 const &) { return 0; } +inline std::size_t hash(T15 const &) { return 0; } + +#endif // variant_CPP11_OR_GREATER + +template +struct helper { + typedef signed char type_index_t; + typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, + T15) variant_types; + + template static U *as(void *data) { return reinterpret_cast(data); } + + template static U const *as(void const *data) { + return reinterpret_cast(data); + } + + static type_index_t to_index_t(std::size_t index) { return static_cast(index); } + + static void destroy(type_index_t index, void *data) { + switch (index) { + case 0: + as(data)->~T0(); + break; + case 1: + as(data)->~T1(); + break; + case 2: + as(data)->~T2(); + break; + case 3: + as(data)->~T3(); + break; + case 4: + as(data)->~T4(); + break; + case 5: + as(data)->~T5(); + break; + case 6: + as(data)->~T6(); + break; + case 7: + as(data)->~T7(); + break; + case 8: + as(data)->~T8(); + break; + case 9: + as(data)->~T9(); + break; + case 10: + as(data)->~T10(); + break; + case 11: + as(data)->~T11(); + break; + case 12: + as(data)->~T12(); + break; + case 13: + as(data)->~T13(); + break; + case 14: + as(data)->~T14(); + break; + case 15: + as(data)->~T15(); + break; + } + } + +#if variant_CPP11_OR_GREATER + template static type_index_t construct_t(void *data, Args &&... args) { + new (data) T(std::forward(args)...); + + return to_index_t(detail::typelist_index_of::value); + } + + template + static type_index_t construct_i(void *data, Args &&... args) { + using type = typename detail::typelist_type_at::type; + + construct_t(data, std::forward(args)...); + + return to_index_t(K); + } + + static type_index_t move_construct(type_index_t const from_index, void *from_value, + void *to_value) { + switch (from_index) { + case 0: + new (to_value) T0(std::move(*as(from_value))); + break; + case 1: + new (to_value) T1(std::move(*as(from_value))); + break; + case 2: + new (to_value) T2(std::move(*as(from_value))); + break; + case 3: + new (to_value) T3(std::move(*as(from_value))); + break; + case 4: + new (to_value) T4(std::move(*as(from_value))); + break; + case 5: + new (to_value) T5(std::move(*as(from_value))); + break; + case 6: + new (to_value) T6(std::move(*as(from_value))); + break; + case 7: + new (to_value) T7(std::move(*as(from_value))); + break; + case 8: + new (to_value) T8(std::move(*as(from_value))); + break; + case 9: + new (to_value) T9(std::move(*as(from_value))); + break; + case 10: + new (to_value) T10(std::move(*as(from_value))); + break; + case 11: + new (to_value) T11(std::move(*as(from_value))); + break; + case 12: + new (to_value) T12(std::move(*as(from_value))); + break; + case 13: + new (to_value) T13(std::move(*as(from_value))); + break; + case 14: + new (to_value) T14(std::move(*as(from_value))); + break; + case 15: + new (to_value) T15(std::move(*as(from_value))); + break; + } + return from_index; + } + + static type_index_t move_assign(type_index_t const from_index, void *from_value, void *to_value) { + switch (from_index) { + case 0: + *as(to_value) = std::move(*as(from_value)); + break; + case 1: + *as(to_value) = std::move(*as(from_value)); + break; + case 2: + *as(to_value) = std::move(*as(from_value)); + break; + case 3: + *as(to_value) = std::move(*as(from_value)); + break; + case 4: + *as(to_value) = std::move(*as(from_value)); + break; + case 5: + *as(to_value) = std::move(*as(from_value)); + break; + case 6: + *as(to_value) = std::move(*as(from_value)); + break; + case 7: + *as(to_value) = std::move(*as(from_value)); + break; + case 8: + *as(to_value) = std::move(*as(from_value)); + break; + case 9: + *as(to_value) = std::move(*as(from_value)); + break; + case 10: + *as(to_value) = std::move(*as(from_value)); + break; + case 11: + *as(to_value) = std::move(*as(from_value)); + break; + case 12: + *as(to_value) = std::move(*as(from_value)); + break; + case 13: + *as(to_value) = std::move(*as(from_value)); + break; + case 14: + *as(to_value) = std::move(*as(from_value)); + break; + case 15: + *as(to_value) = std::move(*as(from_value)); + break; + } + return from_index; + } +#endif + + static type_index_t copy_construct(type_index_t const from_index, const void *from_value, + void *to_value) { + switch (from_index) { + case 0: + new (to_value) T0(*as(from_value)); + break; + case 1: + new (to_value) T1(*as(from_value)); + break; + case 2: + new (to_value) T2(*as(from_value)); + break; + case 3: + new (to_value) T3(*as(from_value)); + break; + case 4: + new (to_value) T4(*as(from_value)); + break; + case 5: + new (to_value) T5(*as(from_value)); + break; + case 6: + new (to_value) T6(*as(from_value)); + break; + case 7: + new (to_value) T7(*as(from_value)); + break; + case 8: + new (to_value) T8(*as(from_value)); + break; + case 9: + new (to_value) T9(*as(from_value)); + break; + case 10: + new (to_value) T10(*as(from_value)); + break; + case 11: + new (to_value) T11(*as(from_value)); + break; + case 12: + new (to_value) T12(*as(from_value)); + break; + case 13: + new (to_value) T13(*as(from_value)); + break; + case 14: + new (to_value) T14(*as(from_value)); + break; + case 15: + new (to_value) T15(*as(from_value)); + break; + } + return from_index; + } + + static type_index_t copy_assign(type_index_t const from_index, const void *from_value, + void *to_value) { + switch (from_index) { + case 0: + *as(to_value) = *as(from_value); + break; + case 1: + *as(to_value) = *as(from_value); + break; + case 2: + *as(to_value) = *as(from_value); + break; + case 3: + *as(to_value) = *as(from_value); + break; + case 4: + *as(to_value) = *as(from_value); + break; + case 5: + *as(to_value) = *as(from_value); + break; + case 6: + *as(to_value) = *as(from_value); + break; + case 7: + *as(to_value) = *as(from_value); + break; + case 8: + *as(to_value) = *as(from_value); + break; + case 9: + *as(to_value) = *as(from_value); + break; + case 10: + *as(to_value) = *as(from_value); + break; + case 11: + *as(to_value) = *as(from_value); + break; + case 12: + *as(to_value) = *as(from_value); + break; + case 13: + *as(to_value) = *as(from_value); + break; + case 14: + *as(to_value) = *as(from_value); + break; + case 15: + *as(to_value) = *as(from_value); + break; + } + return from_index; + } +}; + +} // namespace detail + +// +// Variant: +// + +template +class variant; + +// 19.7.8 Class monostate + +class monostate {}; + +// 19.7.9 monostate relational operators + +inline variant_constexpr bool operator<(monostate, monostate) variant_noexcept { return false; } +inline variant_constexpr bool operator>(monostate, monostate) variant_noexcept { return false; } +inline variant_constexpr bool operator<=(monostate, monostate) variant_noexcept { return true; } +inline variant_constexpr bool operator>=(monostate, monostate) variant_noexcept { return true; } +inline variant_constexpr bool operator==(monostate, monostate) variant_noexcept { return true; } +inline variant_constexpr bool operator!=(monostate, monostate) variant_noexcept { return false; } + +// 19.7.4 variant helper classes + +// obtain the size of the variant's list of alternatives at compile time + +template struct variant_size; /* undefined */ + +template +struct variant_size> { + enum _ { + value = detail::typelist_size::value + }; +}; + +#if variant_CPP14_OR_GREATER +template constexpr std::size_t variant_size_v = variant_size::value; +#endif + +#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO +#define variant_size_V(T) nonstd::variant_size::value +#endif + +// obtain the type of the alternative specified by its index, at compile time: + +template struct variant_alternative; /* undefined */ + +template +struct variant_alternative< + K, variant> { + typedef typename detail::typelist_type_at::type type; +}; + +#if variant_CPP11_OR_GREATER +template +using variant_alternative_t = typename variant_alternative::type; +#endif + +#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO +#define variant_alternative_T(K, T) typename nonstd::variant_alternative::type +#endif + +// NTS:implement specializes the std::uses_allocator type trait +// std::uses_allocator + +// index of the variant in the invalid state (constant) + +#if variant_CPP11_OR_GREATER +variant_constexpr std::size_t variant_npos = static_cast(-1); +#else +static const std::size_t variant_npos = static_cast(-1); +#endif + +#if !variant_CONFIG_NO_EXCEPTIONS + +// 19.7.11 Class bad_variant_access + +class bad_variant_access : public std::exception { +public: +#if variant_CPP11_OR_GREATER + virtual const char *what() const variant_noexcept variant_override +#else + virtual const char *what() const throw() +#endif + { + return "bad variant access"; + } +}; + +#endif // variant_CONFIG_NO_EXCEPTIONS + +// 19.7.3 Class template variant + +template +class variant { + typedef detail::helper + helper_type; + typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, + T15) variant_types; + +public: + // 19.7.3.1 Constructors + + variant() : type_index(0) { new (ptr()) T0(); } + + variant(T0 const &t0) : type_index(0) { new (ptr()) T0(t0); } + variant(T1 const &t1) : type_index(1) { new (ptr()) T1(t1); } + variant(T2 const &t2) : type_index(2) { new (ptr()) T2(t2); } + variant(T3 const &t3) : type_index(3) { new (ptr()) T3(t3); } + variant(T4 const &t4) : type_index(4) { new (ptr()) T4(t4); } + variant(T5 const &t5) : type_index(5) { new (ptr()) T5(t5); } + variant(T6 const &t6) : type_index(6) { new (ptr()) T6(t6); } + variant(T7 const &t7) : type_index(7) { new (ptr()) T7(t7); } + variant(T8 const &t8) : type_index(8) { new (ptr()) T8(t8); } + variant(T9 const &t9) : type_index(9) { new (ptr()) T9(t9); } + variant(T10 const &t10) : type_index(10) { new (ptr()) T10(t10); } + variant(T11 const &t11) : type_index(11) { new (ptr()) T11(t11); } + variant(T12 const &t12) : type_index(12) { new (ptr()) T12(t12); } + variant(T13 const &t13) : type_index(13) { new (ptr()) T13(t13); } + variant(T14 const &t14) : type_index(14) { new (ptr()) T14(t14); } + variant(T15 const &t15) : type_index(15) { new (ptr()) T15(t15); } + +#if variant_CPP11_OR_GREATER + variant(T0 &&t0) : type_index(0) { new (ptr()) T0(std::move(t0)); } + variant(T1 &&t1) : type_index(1) { new (ptr()) T1(std::move(t1)); } + variant(T2 &&t2) : type_index(2) { new (ptr()) T2(std::move(t2)); } + variant(T3 &&t3) : type_index(3) { new (ptr()) T3(std::move(t3)); } + variant(T4 &&t4) : type_index(4) { new (ptr()) T4(std::move(t4)); } + variant(T5 &&t5) : type_index(5) { new (ptr()) T5(std::move(t5)); } + variant(T6 &&t6) : type_index(6) { new (ptr()) T6(std::move(t6)); } + variant(T7 &&t7) : type_index(7) { new (ptr()) T7(std::move(t7)); } + variant(T8 &&t8) : type_index(8) { new (ptr()) T8(std::move(t8)); } + variant(T9 &&t9) : type_index(9) { new (ptr()) T9(std::move(t9)); } + variant(T10 &&t10) : type_index(10) { new (ptr()) T10(std::move(t10)); } + variant(T11 &&t11) : type_index(11) { new (ptr()) T11(std::move(t11)); } + variant(T12 &&t12) : type_index(12) { new (ptr()) T12(std::move(t12)); } + variant(T13 &&t13) : type_index(13) { new (ptr()) T13(std::move(t13)); } + variant(T14 &&t14) : type_index(14) { new (ptr()) T14(std::move(t14)); } + variant(T15 &&t15) : type_index(15) { new (ptr()) T15(std::move(t15)); } + +#endif + + variant(variant const &other) : type_index(other.type_index) { + (void)helper_type::copy_construct(other.type_index, other.ptr(), ptr()); + } + +#if variant_CPP11_OR_GREATER + + variant(variant &&other) noexcept( + std::is_nothrow_move_constructible::value &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value &&std::is_nothrow_move_constructible< + T3>::value &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value &&std::is_nothrow_move_constructible< + T6>::value &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value + &&std::is_nothrow_move_constructible::value) + : type_index(other.type_index) { + (void)helper_type::move_construct(other.type_index, other.ptr(), ptr()); + } + + template + using type_at_t = typename detail::typelist_type_at::type; + + template ::value)> + explicit variant(nonstd_lite_in_place_type_t(T), Args &&... args) { + type_index = variant_npos_internal(); + type_index = helper_type::template construct_t(ptr(), std::forward(args)...); + } + + template &, Args...>::value)> + explicit variant(nonstd_lite_in_place_type_t(T), std::initializer_list il, Args &&... args) { + type_index = variant_npos_internal(); + type_index = helper_type::template construct_t(ptr(), il, std::forward(args)...); + } + + template , Args...>::value)> + explicit variant(nonstd_lite_in_place_index_t(K), Args &&... args) { + type_index = variant_npos_internal(); + type_index = helper_type::template construct_i(ptr(), std::forward(args)...); + } + + template , std::initializer_list &, Args...>::value)> + explicit variant(nonstd_lite_in_place_index_t(K), std::initializer_list il, Args &&... args) { + type_index = variant_npos_internal(); + type_index = helper_type::template construct_i(ptr(), il, std::forward(args)...); + } + +#endif // variant_CPP11_OR_GREATER + + // 19.7.3.2 Destructor + + ~variant() { + if (!valueless_by_exception()) { + helper_type::destroy(type_index, ptr()); + } + } + + // 19.7.3.3 Assignment + + variant &operator=(variant const &other) { return copy_assign(other); } + +#if variant_CPP11_OR_GREATER + + variant &operator=(variant &&other) noexcept( + std::is_nothrow_move_assignable::value &&std::is_nothrow_move_assignable::value + &&std::is_nothrow_move_assignable::value &&std::is_nothrow_move_assignable::value + &&std::is_nothrow_move_assignable::value &&std::is_nothrow_move_assignable< + T5>::value &&std::is_nothrow_move_assignable::value + &&std::is_nothrow_move_assignable::value &&std::is_nothrow_move_assignable< + T8>::value &&std::is_nothrow_move_assignable::value && + std::is_nothrow_move_assignable::value &&std::is_nothrow_move_assignable< + T11>::value &&std::is_nothrow_move_assignable::value + &&std::is_nothrow_move_assignable::value + &&std::is_nothrow_move_assignable::value + &&std::is_nothrow_move_assignable::value) { + return move_assign(std::move(other)); + } + + variant &operator=(T0 &&t0) { return assign_value<0>(std::move(t0)); } + variant &operator=(T1 &&t1) { return assign_value<1>(std::move(t1)); } + variant &operator=(T2 &&t2) { return assign_value<2>(std::move(t2)); } + variant &operator=(T3 &&t3) { return assign_value<3>(std::move(t3)); } + variant &operator=(T4 &&t4) { return assign_value<4>(std::move(t4)); } + variant &operator=(T5 &&t5) { return assign_value<5>(std::move(t5)); } + variant &operator=(T6 &&t6) { return assign_value<6>(std::move(t6)); } + variant &operator=(T7 &&t7) { return assign_value<7>(std::move(t7)); } + variant &operator=(T8 &&t8) { return assign_value<8>(std::move(t8)); } + variant &operator=(T9 &&t9) { return assign_value<9>(std::move(t9)); } + variant &operator=(T10 &&t10) { return assign_value<10>(std::move(t10)); } + variant &operator=(T11 &&t11) { return assign_value<11>(std::move(t11)); } + variant &operator=(T12 &&t12) { return assign_value<12>(std::move(t12)); } + variant &operator=(T13 &&t13) { return assign_value<13>(std::move(t13)); } + variant &operator=(T14 &&t14) { return assign_value<14>(std::move(t14)); } + variant &operator=(T15 &&t15) { return assign_value<15>(std::move(t15)); } + +#endif + + variant &operator=(T0 const &t0) { return assign_value<0>(t0); } + variant &operator=(T1 const &t1) { return assign_value<1>(t1); } + variant &operator=(T2 const &t2) { return assign_value<2>(t2); } + variant &operator=(T3 const &t3) { return assign_value<3>(t3); } + variant &operator=(T4 const &t4) { return assign_value<4>(t4); } + variant &operator=(T5 const &t5) { return assign_value<5>(t5); } + variant &operator=(T6 const &t6) { return assign_value<6>(t6); } + variant &operator=(T7 const &t7) { return assign_value<7>(t7); } + variant &operator=(T8 const &t8) { return assign_value<8>(t8); } + variant &operator=(T9 const &t9) { return assign_value<9>(t9); } + variant &operator=(T10 const &t10) { return assign_value<10>(t10); } + variant &operator=(T11 const &t11) { return assign_value<11>(t11); } + variant &operator=(T12 const &t12) { return assign_value<12>(t12); } + variant &operator=(T13 const &t13) { return assign_value<13>(t13); } + variant &operator=(T14 const &t14) { return assign_value<14>(t14); } + variant &operator=(T15 const &t15) { return assign_value<15>(t15); } + + std::size_t index() const { + return variant_npos_internal() == type_index ? variant_npos + : static_cast(type_index); + } + + // 19.7.3.4 Modifiers + +#if variant_CPP11_OR_GREATER + template ::value)> + T &emplace(Args &&... args) { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + type_index = helper_type::template construct_t(ptr(), std::forward(args)...); + + return *as(); + } + + template &, Args...>::value)> + T &emplace(std::initializer_list il, Args &&... args) { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + type_index = helper_type::template construct_t(ptr(), il, std::forward(args)...); + + return *as(); + } + + template , Args...>::value)> + variant_alternative_t &emplace(Args &&... args) { + return this->template emplace>(std::forward(args)...); + } + + template , std::initializer_list &, Args...>::value)> + variant_alternative_t &emplace(std::initializer_list il, Args &&... args) { + return this->template emplace>(il, std::forward(args)...); + } + +#endif // variant_CPP11_OR_GREATER + + // 19.7.3.5 Value status + + bool valueless_by_exception() const { return type_index == variant_npos_internal(); } + + // 19.7.3.6 Swap + + void swap(variant &other) +#if variant_CPP11_OR_GREATER + noexcept( + std::is_nothrow_move_constructible::value &&std17::is_nothrow_swappable< + T0>::value &&std::is_nothrow_move_constructible::value + &&std17::is_nothrow_swappable::value &&std::is_nothrow_move_constructible< + T2>::value &&std17::is_nothrow_swappable::value + &&std::is_nothrow_move_constructible::value &&std17::is_nothrow_swappable< + T3>::value &&std::is_nothrow_move_constructible::value + &&std17::is_nothrow_swappable::value &&std::is_nothrow_move_constructible< + T5>::value &&std17::is_nothrow_swappable::value &&std:: + is_nothrow_move_constructible::value &&std17::is_nothrow_swappable< + T6>::value &&std::is_nothrow_move_constructible::value &&std17:: + is_nothrow_swappable::value &&std::is_nothrow_move_constructible< + T8>::value &&std17::is_nothrow_swappable::value + &&std::is_nothrow_move_constructible< + T9>::value &&std17::is_nothrow_swappable::value + &&std::is_nothrow_move_constructible< + T10>::value &&std17::is_nothrow_swappable::value + &&std::is_nothrow_move_constructible< + T11>::value &&std17::is_nothrow_swappable::value + &&std::is_nothrow_move_constructible::value + &&std17::is_nothrow_swappable::value && + std::is_nothrow_move_constructible::value + &&std17::is_nothrow_swappable::value + &&std::is_nothrow_move_constructible< + T14>::value + &&std17::is_nothrow_swappable< + T14>::value &&std:: + is_nothrow_move_constructible< + T15>::value &&std17:: + is_nothrow_swappable< + T15>::value + + ) +#endif + { + if (valueless_by_exception() && other.valueless_by_exception()) { + // no effect + } else if (type_index == other.type_index) { + this->swap_value(type_index, other); + } else { +#if variant_CPP11_OR_GREATER + variant tmp(std::move(*this)); + *this = std::move(other); + other = std::move(tmp); +#else + variant tmp(*this); + *this = other; + other = tmp; +#endif + } + } + + // + // non-standard: + // + + template static variant_constexpr std::size_t index_of() variant_noexcept { + return to_size_t( + detail::typelist_index_of::type>::value); + } + + template T &get() { +#if variant_CONFIG_NO_EXCEPTIONS + assert(index_of() == index()); +#else + if (index_of() != index()) { + throw bad_variant_access(); + } +#endif + return *as(); + } + + template T const &get() const { +#if variant_CONFIG_NO_EXCEPTIONS + assert(index_of() == index()); +#else + if (index_of() != index()) { + throw bad_variant_access(); + } +#endif + return *as(); + } + + template typename variant_alternative::type &get() { + return this->template get::type>(); + } + + template typename variant_alternative::type const &get() const { + return this->template get::type>(); + } + +private: + typedef typename helper_type::type_index_t type_index_t; + + void *ptr() variant_noexcept { return &data; } + + void const *ptr() const variant_noexcept { return &data; } + + template U *as() { return reinterpret_cast(ptr()); } + + template U const *as() const { return reinterpret_cast(ptr()); } + + template static variant_constexpr std::size_t to_size_t(U index) { + return static_cast(index); + } + + variant_constexpr type_index_t variant_npos_internal() const variant_noexcept { + return static_cast(-1); + } + + variant ©_assign(variant const &other) { + if (valueless_by_exception() && other.valueless_by_exception()) { + // no effect + } else if (!valueless_by_exception() && other.valueless_by_exception()) { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + } else if (index() == other.index()) { + type_index = helper_type::copy_assign(other.type_index, other.ptr(), ptr()); + } else { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + type_index = helper_type::copy_construct(other.type_index, other.ptr(), ptr()); + } + return *this; + } + +#if variant_CPP11_OR_GREATER + + variant &move_assign(variant &&other) { + if (valueless_by_exception() && other.valueless_by_exception()) { + // no effect + } else if (!valueless_by_exception() && other.valueless_by_exception()) { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + } else if (index() == other.index()) { + type_index = helper_type::move_assign(other.type_index, other.ptr(), ptr()); + } else { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + type_index = helper_type::move_construct(other.type_index, other.ptr(), ptr()); + } + return *this; + } + + template variant &assign_value(T &&value) { + if (index() == K) { + *as() = std::forward(value); + } else { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + new (ptr()) T(std::forward(value)); + type_index = K; + } + return *this; + } + +#endif // variant_CPP11_OR_GREATER + + template variant &assign_value(T const &value) { + if (index() == K) { + *as() = value; + } else { + helper_type::destroy(type_index, ptr()); + type_index = variant_npos_internal(); + new (ptr()) T(value); + type_index = K; + } + return *this; + } + + void swap_value(type_index_t index, variant &other) { + using std::swap; + switch (index) { + case 0: + swap(this->get<0>(), other.get<0>()); + break; + case 1: + swap(this->get<1>(), other.get<1>()); + break; + case 2: + swap(this->get<2>(), other.get<2>()); + break; + case 3: + swap(this->get<3>(), other.get<3>()); + break; + case 4: + swap(this->get<4>(), other.get<4>()); + break; + case 5: + swap(this->get<5>(), other.get<5>()); + break; + case 6: + swap(this->get<6>(), other.get<6>()); + break; + case 7: + swap(this->get<7>(), other.get<7>()); + break; + case 8: + swap(this->get<8>(), other.get<8>()); + break; + case 9: + swap(this->get<9>(), other.get<9>()); + break; + case 10: + swap(this->get<10>(), other.get<10>()); + break; + case 11: + swap(this->get<11>(), other.get<11>()); + break; + case 12: + swap(this->get<12>(), other.get<12>()); + break; + case 13: + swap(this->get<13>(), other.get<13>()); + break; + case 14: + swap(this->get<14>(), other.get<14>()); + break; + case 15: + swap(this->get<15>(), other.get<15>()); + break; + } + } + +private: + enum { data_size = detail::typelist_max::value }; + +#if variant_CPP11_OR_GREATER + + enum { data_align = detail::typelist_max_alignof::value }; + + using aligned_storage_t = typename std::aligned_storage::type; + aligned_storage_t data; + +#elif variant_CONFIG_MAX_ALIGN_HACK + + typedef union { + unsigned char data[data_size]; + } aligned_storage_t; + + detail::max_align_t hack; + aligned_storage_t data; + +#else + typedef typename detail::typelist_max::type max_type; + + typedef variant_ALIGN_AS(max_type) align_as_type; + + typedef union { + align_as_type data[1 + (data_size - 1) / sizeof(align_as_type)]; + } aligned_storage_t; + aligned_storage_t data; + + // # undef variant_ALIGN_AS + +#endif // variant_CONFIG_MAX_ALIGN_HACK + + type_index_t type_index; +}; + +// 19.7.5 Value access + +template +inline bool holds_alternative( + variant const &v) + variant_noexcept { + return v.index() == variant::template index_of(); +} + +template +inline R &get(variant &v, + nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) { + return v.template get(); +} + +template +inline R const & +get(variant const &v, + nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) { + return v.template get(); +} + +template +inline typename variant_alternative< + K, variant>::type & +get(variant &v, + nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) { +#if variant_CONFIG_NO_EXCEPTIONS + assert(K == v.index()); +#else + if (K != v.index()) { + throw bad_variant_access(); + } +#endif + return v.template get(); +} + +template +inline typename variant_alternative< + K, variant>::type const & +get(variant const &v, + nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) { +#if variant_CONFIG_NO_EXCEPTIONS + assert(K == v.index()); +#else + if (K != v.index()) { + throw bad_variant_access(); + } +#endif + return v.template get(); +} + +#if variant_CPP11_OR_GREATER + +template +inline R &&get(variant &&v, + nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) { + return std::move(v.template get()); +} + +template +inline R const && +get(variant const &&v, + nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) { + return std::move(v.template get()); +} + +template +inline typename variant_alternative< + K, variant>::type && +get(variant &&v, + nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) { +#if variant_CONFIG_NO_EXCEPTIONS + assert(K == v.index()); +#else + if (K != v.index()) { + throw bad_variant_access(); + } +#endif + return std::move(v.template get()); +} + +template +inline typename variant_alternative< + K, variant>::type const && +get(variant const &&v, + nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) { +#if variant_CONFIG_NO_EXCEPTIONS + assert(K == v.index()); +#else + if (K != v.index()) { + throw bad_variant_access(); + } +#endif + return std::move(v.template get()); +} + +#endif // variant_CPP11_OR_GREATER + +template +inline typename std11::add_pointer::type +get_if(variant *pv, + nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) { + return (pv->index() == variant::template index_of()) + ? &get(*pv) + : variant_nullptr; +} + +template +inline typename std11::add_pointer::type +get_if(variant const *pv, + nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) { + return (pv->index() == variant::template index_of()) + ? &get(*pv) + : variant_nullptr; +} + +template +inline typename std11::add_pointer>::type>::type +get_if(variant *pv, + nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) { + return (pv->index() == K) ? &get(*pv) : variant_nullptr; +} + +template +inline typename std11::add_pointer>::type>::type +get_if(variant const *pv, + nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) { + return (pv->index() == K) ? &get(*pv) : variant_nullptr; +} + +// 19.7.10 Specialized algorithms + +template < + class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, + class T9, class T10, class T11, class T12, class T13, class T14, + class T15 +#if variant_CPP11_OR_GREATER + variant_REQUIRES_T( + std::is_move_constructible::value &&std17::is_swappable< + T0>::value &&std::is_move_constructible::value &&std17::is_swappable::value + &&std::is_move_constructible::value &&std17::is_swappable< + T2>::value &&std::is_move_constructible::value &&std17::is_swappable:: + value &&std::is_move_constructible::value &&std17::is_swappable::value + &&std::is_move_constructible::value &&std17::is_swappable< + T5>::value &&std::is_move_constructible::value + &&std17::is_swappable::value &&std::is_move_constructible< + T7>::value &&std17::is_swappable::value + &&std::is_move_constructible::value &&std17::is_swappable< + T8>::value &&std::is_move_constructible::value + &&std17::is_swappable::value &&std::is_move_constructible< + T10>::value &&std17::is_swappable::value &&std:: + is_move_constructible::value &&std17::is_swappable< + T11>::value &&std::is_move_constructible::value + &&std17::is_swappable< + T12>::value &&std::is_move_constructible::value + &&std17::is_swappable::value + &&std::is_move_constructible::value + &&std17::is_swappable::value + &&std::is_move_constructible::value + &&std17::is_swappable::value) +#endif + > +inline void swap(variant &a, + variant &b) +#if variant_CPP11_OR_GREATER + noexcept(noexcept(a.swap(b))) +#endif +{ + a.swap(b); +} + +// 19.7.7 Visitation + +// Variant 'visitor' implementation + +namespace detail { + +template struct VisitorApplicatorImpl { + template static R apply(Visitor const &v, T const &arg) { + return v(arg); + } +}; + +template struct VisitorApplicatorImpl> { + template static R apply(Visitor const &, T) { + // prevent default construction of a const reference, see issue #39: + std::terminate(); + } +}; + +template struct VisitorApplicator; + +template struct VisitorUnwrapper; + +#if variant_CPP11_OR_GREATER +template +#else +template +#endif +struct TypedVisitorUnwrapper; + +template +struct TypedVisitorUnwrapper<2, R, Visitor, T2> { + const Visitor &visitor; + T2 const &val2; + + TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_) + : visitor(visitor_), val2(val2_) + + {} + + template R operator()(const T &val1) const { return visitor(val1, val2); } +}; + +template +struct TypedVisitorUnwrapper<3, R, Visitor, T2, T3> { + const Visitor &visitor; + T2 const &val2; + T3 const &val3; + + TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_) + : visitor(visitor_), val2(val2_), val3(val3_) + + {} + + template R operator()(const T &val1) const { return visitor(val1, val2, val3); } +}; + +template +struct TypedVisitorUnwrapper<4, R, Visitor, T2, T3, T4> { + const Visitor &visitor; + T2 const &val2; + T3 const &val3; + T4 const &val4; + + TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_) + : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_) + + {} + + template R operator()(const T &val1) const { + return visitor(val1, val2, val3, val4); + } +}; + +template +struct TypedVisitorUnwrapper<5, R, Visitor, T2, T3, T4, T5> { + const Visitor &visitor; + T2 const &val2; + T3 const &val3; + T4 const &val4; + T5 const &val5; + + TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_, + T5 const &val5_) + : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_), val5(val5_) + + {} + + template R operator()(const T &val1) const { + return visitor(val1, val2, val3, val4, val5); + } +}; + +template struct VisitorUnwrapper { + const Visitor &visitor; + const V2 &r; + + VisitorUnwrapper(const Visitor &visitor_, const V2 &r_) : visitor(visitor_), r(r_) {} + + template R operator()(T1 const &val1) const { + typedef TypedVisitorUnwrapper<2, R, Visitor, T1> visitor_type; + return VisitorApplicator::apply(visitor_type(visitor, val1), r); + } + + template R operator()(T1 const &val1, T2 const &val2) const { + typedef TypedVisitorUnwrapper<3, R, Visitor, T1, T2> visitor_type; + return VisitorApplicator::apply(visitor_type(visitor, val1, val2), r); + } + + template + R operator()(T1 const &val1, T2 const &val2, T3 const &val3) const { + typedef TypedVisitorUnwrapper<4, R, Visitor, T1, T2, T3> visitor_type; + return VisitorApplicator::apply(visitor_type(visitor, val1, val2, val3), r); + } + + template + R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4) const { + typedef TypedVisitorUnwrapper<5, R, Visitor, T1, T2, T3, T4> visitor_type; + return VisitorApplicator::apply(visitor_type(visitor, val1, val2, val3, val4), r); + } + + template + R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4, + T5 const &val5) const { + typedef TypedVisitorUnwrapper<6, R, Visitor, T1, T2, T3, T4, T5> visitor_type; + return VisitorApplicator::apply(visitor_type(visitor, val1, val2, val3, val4, val5), r); + } +}; + +template struct VisitorApplicator { + template static R apply(const Visitor &v, const V1 &arg) { + switch (arg.index()) { + case 0: + return apply_visitor<0>(v, arg); + case 1: + return apply_visitor<1>(v, arg); + case 2: + return apply_visitor<2>(v, arg); + case 3: + return apply_visitor<3>(v, arg); + case 4: + return apply_visitor<4>(v, arg); + case 5: + return apply_visitor<5>(v, arg); + case 6: + return apply_visitor<6>(v, arg); + case 7: + return apply_visitor<7>(v, arg); + case 8: + return apply_visitor<8>(v, arg); + case 9: + return apply_visitor<9>(v, arg); + case 10: + return apply_visitor<10>(v, arg); + case 11: + return apply_visitor<11>(v, arg); + case 12: + return apply_visitor<12>(v, arg); + case 13: + return apply_visitor<13>(v, arg); + case 14: + return apply_visitor<14>(v, arg); + case 15: + return apply_visitor<15>(v, arg); + + // prevent default construction of a const reference, see issue #39: + default: + std::terminate(); + } + } + + template + static R apply_visitor(const Visitor &v, const V1 &arg) { + +#if variant_CPP11_OR_GREATER + typedef typename variant_alternative::type>::type value_type; +#else + typedef typename variant_alternative::type value_type; +#endif + return VisitorApplicatorImpl::apply(v, get(arg)); + } + +#if variant_CPP11_OR_GREATER + template + static R apply(const Visitor &v, const V1 &arg1, const V2 &arg2, const V... args) { + typedef VisitorUnwrapper Unwrapper; + Unwrapper unwrapper(v, arg1); + return apply(unwrapper, arg2, args...); + } +#else + + template + static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2) { + typedef VisitorUnwrapper Unwrapper; + Unwrapper unwrapper(v, arg1); + return apply(unwrapper, arg2); + } + + template + static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) { + typedef VisitorUnwrapper Unwrapper; + Unwrapper unwrapper(v, arg1); + return apply(unwrapper, arg2, arg3); + } + + template + static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) { + typedef VisitorUnwrapper Unwrapper; + Unwrapper unwrapper(v, arg1); + return apply(unwrapper, arg2, arg3, arg4); + } + + template + static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4, + V5 const &arg5) { + typedef VisitorUnwrapper Unwrapper; + Unwrapper unwrapper(v, arg1); + return apply(unwrapper, arg2, arg3, arg4, arg5); + } + +#endif +}; + +#if variant_CPP11_OR_GREATER +template struct VisitorImpl { + typedef decltype( + std::declval()(get<0>(static_cast(std::declval()))...)) result_type; + typedef VisitorApplicator applicator_type; +}; +#endif +} // namespace detail + +#if variant_CPP11_OR_GREATER +// No perfect forwarding here in order to simplify code +template +inline auto visit(Visitor const &v, V const &... vars) -> + typename detail::VisitorImpl::result_type { + typedef detail::VisitorImpl impl_type; + return impl_type::applicator_type::apply(v, vars...); +} +#else + +template +inline R visit(const Visitor &v, V1 const &arg1) { + return detail::VisitorApplicator::apply(v, arg1); +} + +template +inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2) { + return detail::VisitorApplicator::apply(v, arg1, arg2); +} + +template +inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) { + return detail::VisitorApplicator::apply(v, arg1, arg2, arg3); +} + +template +inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) { + return detail::VisitorApplicator::apply(v, arg1, arg2, arg3, arg4); +} + +template +inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4, + V5 const &arg5) { + return detail::VisitorApplicator::apply(v, arg1, arg2, arg3, arg4, arg5); +} + +#endif + +// 19.7.6 Relational operators + +namespace detail { + +template struct Comparator { + static inline bool equal(Variant const &v, Variant const &w) { + switch (v.index()) { + case 0: + return get<0>(v) == get<0>(w); + case 1: + return get<1>(v) == get<1>(w); + case 2: + return get<2>(v) == get<2>(w); + case 3: + return get<3>(v) == get<3>(w); + case 4: + return get<4>(v) == get<4>(w); + case 5: + return get<5>(v) == get<5>(w); + case 6: + return get<6>(v) == get<6>(w); + case 7: + return get<7>(v) == get<7>(w); + case 8: + return get<8>(v) == get<8>(w); + case 9: + return get<9>(v) == get<9>(w); + case 10: + return get<10>(v) == get<10>(w); + case 11: + return get<11>(v) == get<11>(w); + case 12: + return get<12>(v) == get<12>(w); + case 13: + return get<13>(v) == get<13>(w); + case 14: + return get<14>(v) == get<14>(w); + case 15: + return get<15>(v) == get<15>(w); + + default: + return false; + } + } + + static inline bool less_than(Variant const &v, Variant const &w) { + switch (v.index()) { + case 0: + return get<0>(v) < get<0>(w); + case 1: + return get<1>(v) < get<1>(w); + case 2: + return get<2>(v) < get<2>(w); + case 3: + return get<3>(v) < get<3>(w); + case 4: + return get<4>(v) < get<4>(w); + case 5: + return get<5>(v) < get<5>(w); + case 6: + return get<6>(v) < get<6>(w); + case 7: + return get<7>(v) < get<7>(w); + case 8: + return get<8>(v) < get<8>(w); + case 9: + return get<9>(v) < get<9>(w); + case 10: + return get<10>(v) < get<10>(w); + case 11: + return get<11>(v) < get<11>(w); + case 12: + return get<12>(v) < get<12>(w); + case 13: + return get<13>(v) < get<13>(w); + case 14: + return get<14>(v) < get<14>(w); + case 15: + return get<15>(v) < get<15>(w); + + default: + return false; + } + } +}; + +} // namespace detail + +template +inline bool +operator==(variant const &v, + variant const &w) { + if (v.index() != w.index()) + return false; + else if (v.valueless_by_exception()) + return true; + else + return detail::Comparator< + variant>::equal(v, w); +} + +template +inline bool +operator!=(variant const &v, + variant const &w) { + return !(v == w); +} + +template +inline bool +operator<(variant const &v, + variant const &w) { + if (w.valueless_by_exception()) + return false; + else if (v.valueless_by_exception()) + return true; + else if (v.index() < w.index()) + return true; + else if (v.index() > w.index()) + return false; + else + return detail::Comparator>::less_than(v, w); +} + +template +inline bool +operator>(variant const &v, + variant const &w) { + return w < v; +} + +template +inline bool +operator<=(variant const &v, + variant const &w) { + return !(v > w); +} + +template +inline bool +operator>=(variant const &v, + variant const &w) { + return !(v < w); +} + +} // namespace variants + +using namespace variants; + +} // namespace nonstd + +#if variant_CPP11_OR_GREATER + +// 19.7.12 Hash support + +namespace std { + +template <> struct hash { + std::size_t operator()(nonstd::monostate) const variant_noexcept { return 42; } +}; + +template +struct hash> { + std::size_t operator()(nonstd::variant const &v) const variant_noexcept { + namespace nvd = nonstd::variants::detail; + + switch (v.index()) { + case 0: + return nvd::hash(0) ^ nvd::hash(get<0>(v)); + case 1: + return nvd::hash(1) ^ nvd::hash(get<1>(v)); + case 2: + return nvd::hash(2) ^ nvd::hash(get<2>(v)); + case 3: + return nvd::hash(3) ^ nvd::hash(get<3>(v)); + case 4: + return nvd::hash(4) ^ nvd::hash(get<4>(v)); + case 5: + return nvd::hash(5) ^ nvd::hash(get<5>(v)); + case 6: + return nvd::hash(6) ^ nvd::hash(get<6>(v)); + case 7: + return nvd::hash(7) ^ nvd::hash(get<7>(v)); + case 8: + return nvd::hash(8) ^ nvd::hash(get<8>(v)); + case 9: + return nvd::hash(9) ^ nvd::hash(get<9>(v)); + case 10: + return nvd::hash(10) ^ nvd::hash(get<10>(v)); + case 11: + return nvd::hash(11) ^ nvd::hash(get<11>(v)); + case 12: + return nvd::hash(12) ^ nvd::hash(get<12>(v)); + case 13: + return nvd::hash(13) ^ nvd::hash(get<13>(v)); + case 14: + return nvd::hash(14) ^ nvd::hash(get<14>(v)); + case 15: + return nvd::hash(15) ^ nvd::hash(get<15>(v)); + + default: + return 0; + } + } +}; + +} // namespace std + +#endif // variant_CPP11_OR_GREATER + +#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900) +#pragma warning(pop) +#endif + +#endif // variant_USES_STD_VARIANT + +#endif // NONSTD_VARIANT_LITE_HPP +// +// 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 2 +#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 + +#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 defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) +#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( ) +#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 : + +#if optional_CPP17_OR_GREATER + +#include + +namespace nonstd { + +using std::in_place; +using std::in_place_index; +using std::in_place_index_t; +using std::in_place_t; +using std::in_place_type; +using std::in_place_type_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 +#define nonstd_lite_in_place_index_t(K) std::in_place_index_t + +#define nonstd_lite_in_place(T) \ + std::in_place_t {} +#define nonstd_lite_in_place_type(T) \ + std::in_place_type_t {} +#define nonstd_lite_in_place_index(K) \ + std::in_place_index_t {} + +} // namespace nonstd + +#else // optional_CPP17_OR_GREATER + +#include + +namespace nonstd { +namespace detail { + +template struct in_place_type_tag {}; + +template struct in_place_index_tag {}; + +} // namespace detail + +struct in_place_t {}; + +template +inline in_place_t +in_place(detail::in_place_type_tag /*unused*/ = detail::in_place_type_tag()) { + return in_place_t(); +} + +template +inline in_place_t +in_place(detail::in_place_index_tag /*unused*/ = detail::in_place_index_tag()) { + return in_place_t(); +} + +template +inline in_place_t +in_place_type(detail::in_place_type_tag /*unused*/ = detail::in_place_type_tag()) { + return in_place_t(); +} + +template +inline in_place_t +in_place_index(detail::in_place_index_tag /*unused*/ = detail::in_place_index_tag()) { + return in_place_t(); +} + +// mimic templated typedef: + +#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag) +#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag) +#define nonstd_lite_in_place_index_t(K) \ + nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag) + +#define nonstd_lite_in_place(T) nonstd::in_place_type +#define nonstd_lite_in_place_type(T) nonstd::in_place_type +#define nonstd_lite_in_place_index(K) nonstd::in_place_index + +} // namespace nonstd + +#endif // optional_CPP17_OR_GREATER +#endif // nonstd_lite_HAVE_IN_PLACE_TYPES + +// +// Using std::optional: +// + +#if optional_USES_STD_OPTIONAL + +#include + +namespace nonstd { + +using std::bad_optional_access; +using std::hash; +using std::optional; + +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; +} // namespace nonstd + +#else // optional_USES_STD_OPTIONAL + +#include +#include + +// 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_CPP11_140_490 \ + ((optional_CPP11_OR_GREATER_ && optional_COMPILER_GNUC_VERSION >= 490) || \ + (optional_COMPILER_MSVC_VER >= 1910)) + +#define optional_CPP14_000 (optional_CPP14_OR_GREATER) +#define optional_CPP17_000 (optional_CPP17_OR_GREATER) + +// 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_490 +#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) + +// 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: +#else +#include +#endif + +#if optional_CPP11_OR_GREATER +#include +#endif + +#if optional_HAVE(INITIALIZER_LIST) +#include +#endif + +#if optional_HAVE(TYPE_TRAITS) +#include +#elif optional_HAVE(TR1_TYPE_TRAITS) +#include +#endif + +// Method enabling + +#if optional_CPP11_OR_GREATER + +#define optional_REQUIRES_0(...) \ + template ::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 { + +#if optional_CPP11_OR_GREATER +using std::move; +#else +template T &move(T &t) { return t; } +#endif + +#if optional_HAVE(CONDITIONAL) +using std::conditional; +#else +template struct conditional { typedef T type; }; +template struct conditional { typedef F type; }; +#endif // optional_HAVE_CONDITIONAL + +// gcc < 5: +#if optional_CPP11_OR_GREATER +#if optional_BETWEEN(optional_COMPILER_GNUC_VERSION, 1, 500) +template struct is_trivially_copy_constructible : std::true_type {}; +template struct is_trivially_move_constructible : std::true_type {}; +#else +using std::is_trivially_copy_constructible; +using std::is_trivially_move_constructible; +#endif +#endif +} // namespace std11 + +#if optional_CPP11_OR_GREATER + +/// type traits C++17: + +namespace std17 { + +#if optional_CPP17_OR_GREATER + +using std::is_nothrow_swappable; +using std::is_swappable; + +#elif optional_CPP11_OR_GREATER + +namespace detail { + +using std::swap; + +struct is_swappable { + template (), std::declval()))> + static std::true_type test(int /*unused*/); + + template static std::false_type test(...); +}; + +struct is_nothrow_swappable { + // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015): + + template static constexpr bool satisfies() { + return noexcept(swap(std::declval(), std::declval())); + } + + template + static auto test(int /*unused*/) -> std::integral_constant()> {} + + template static auto test(...) -> std::false_type; +}; + +} // namespace detail + +// is [nothow] swappable: + +template struct is_swappable : decltype(detail::is_swappable::test(0)) {}; + +template +struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test(0)) {}; + +#endif // optional_CPP17_OR_GREATER + +} // namespace std17 + +/// type traits C++20: + +namespace std20 { + +template struct remove_cvref { + typedef typename std::remove_cv::type>::type type; +}; + +} // namespace std20 + +#endif // optional_CPP11_OR_GREATER + +/// class optional + +template class optional; + +namespace detail { + +// C++11 emulation: + +struct nulltype {}; + +template 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 optional_UNIQUE(_st) + +template 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 optional_UNIQUE(_); + struct_t optional_UNIQUE(_); + struct_t 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::value>::type + +template struct alignment_of; + +template struct alignment_of_hack { + char c; + T t; + alignment_of_hack(); +}; + +template struct alignment_logic { + enum { value = A < S ? A : S }; +}; + +template struct alignment_of { + enum { value = alignment_logic) - sizeof(T), sizeof(T)>::value }; +}; + +template struct type_of_size { + typedef + typename std11::conditional::type>::type type; +}; + +template struct type_of_size { + typedef optional_CONFIG_ALIGN_AS_FALLBACK type; +}; + +template struct struct_t { T _; }; + +#define optional_ALIGN_TYPE(type) typelist < type, typelist < struct_t + +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 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 void emplace(Args &&... args) { + ::new (value_ptr()) value_type(std::forward(args)...); + } + + template void emplace(std::initializer_list il, Args &&... args) { + ::new (value_ptr()) value_type(il, std::forward(args)...); + } + +#endif + + void destruct_value() { value_ptr()->~T(); } + + optional_nodiscard value_type const *value_ptr() const { return as(); } + + value_type *value_ptr() { return as(); } + + 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::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 optional_nodiscard U *as() { return reinterpret_cast(ptr()); } + + template optional_nodiscard U const *as() const { + return reinterpret_cast(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 class optional { +private: + template 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::value + // || std11::is_trivially_copy_constructible::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 ::value || + std11::is_trivially_move_constructible::value)> + optional_constexpr14 optional(optional &&other) + // NOLINTNEXTLINE( performance-noexcept-move-constructor ) + noexcept(std::is_nothrow_move_constructible::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 ::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible const &>::value && + !std::is_constructible const &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible const &, T>::value && + !std::is_convertible const &&, T>::value && + !std::is_convertible::value /*=> explicit + */ + )> + explicit optional(optional 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 ::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible const &>::value && + !std::is_constructible const &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible const &, T>::value && + !std::is_convertible const &&, T>::value && + std::is_convertible::value /*=> non-explicit */ + ) +#endif // optional_CPP11_OR_GREATER + > + // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions ) + /*non-explicit*/ optional(optional 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 ::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible const &>::value && + !std::is_constructible const &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible const &, T>::value && + !std::is_convertible const &&, T>::value && + !std::is_convertible::value /*=> explicit */ + )> + explicit optional(optional &&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 ::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible const &>::value && + !std::is_constructible const &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible const &, T>::value && + !std::is_convertible const &&, T>::value && + std::is_convertible::value /*=> non-explicit */ + )> + // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions ) + /*non-explicit*/ optional(optional &&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 ::value)> + optional_constexpr explicit optional(nonstd_lite_in_place_t(T), Args &&... args) + : has_value_(true), contained(T(std::forward(args)...)) {} + + // 7 (C++11) - in-place construct, initializer-list + template &, Args &&...>::value)> + optional_constexpr explicit optional(nonstd_lite_in_place_t(T), std::initializer_list il, + Args &&... args) + : has_value_(true), contained(T(il, std::forward(args)...)) {} + + // 8a (C++11) - explicit move construct from value + template < + typename U = T optional_REQUIRES_T( + std::is_constructible::value && + !std::is_same::type, nonstd_lite_in_place_t(U)>::value && + !std::is_same::type, optional>::value && + !std::is_convertible::value /*=> explicit */ + )> + optional_constexpr explicit optional(U &&value) + : has_value_(true), contained(T{std::forward(value)}) {} + + // 8b (C++11) - non-explicit move construct from value + template < + typename U = T optional_REQUIRES_T( + std::is_constructible::value && + !std::is_same::type, nonstd_lite_in_place_t(U)>::value && + !std::is_same::type, optional>::value && + std::is_convertible::value /*=> non-explicit */ + )> + // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions ) + optional_constexpr /*non-explicit*/ optional(U &&value) + : has_value_(true), contained(std::forward(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::value + // && std::is_copy_assignable::value + ) + operator=(optional const &other) noexcept( + std::is_nothrow_move_assignable::value &&std::is_nothrow_move_constructible::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 + // std::is_move_constructible::value + // && std::is_move_assignable::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 + // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, + // misc-unconventional-assign-operator ) + optional_REQUIRES_R( + optional &, + std::is_constructible::value &&std::is_assignable::value && + !std::is_same::type, nonstd_lite_in_place_t(U)>::value && + !std::is_same::type, optional>::value && + !(std::is_scalar::value && std::is_same::type>::value)) + operator=(U &&value) { + if (has_value()) { + contained.value() = std::forward(value); + } else { + initialize(T(std::forward(value))); + } + return *this; + } + +#else // optional_CPP11_OR_GREATER + + // 4 (C++98) - copy-assign from value + template 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 +#if optional_CPP11_OR_GREATER + // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, + // misc-unconventional-assign-operator ) + optional_REQUIRES_R( + optional &, + std::is_constructible::value &&std::is_assignable::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible const &>::value && + !std::is_constructible const &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible const &, T>::value && + !std::is_convertible const &&, T>::value && + !std::is_assignable &>::value && + !std::is_assignable &&>::value && + !std::is_assignable const &>::value && + !std::is_assignable const &&>::value) +#else + optional & +#endif // optional_CPP11_OR_GREATER + operator=(optional const &other) { + return *this = optional(other); + } + +#if optional_CPP11_OR_GREATER + + // 6 (C++11) - converting move-assign from optional + template + // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, + // misc-unconventional-assign-operator ) + optional_REQUIRES_R(optional &, + std::is_constructible::value &&std::is_assignable::value && + !std::is_constructible &>::value && + !std::is_constructible &&>::value && + !std::is_constructible const &>::value && + !std::is_constructible const &&>::value && + !std::is_convertible &, T>::value && + !std::is_convertible &&, T>::value && + !std::is_convertible const &, T>::value && + !std::is_convertible const &&, T>::value && + !std::is_assignable &>::value && + !std::is_assignable &&>::value && + !std::is_assignable const &>::value && + !std::is_assignable const &&>::value) + operator=(optional &&other) { + return *this = optional(std::move(other)); + } + + // 7 (C++11) - emplace + template ::value)> + T &emplace(Args &&... args) { + *this = nullopt; + contained.emplace(std::forward(args)...); + has_value_ = true; + return contained.value(); + } + + // 8 (C++11) - emplace, initializer-list + template &, Args &&...>::value)> + T &emplace(std::initializer_list il, Args &&... args) { + *this = nullopt; + contained.emplace(il, std::forward(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(std::is_nothrow_move_constructible::value &&std17::is_nothrow_swappable::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_CPP11_OR_GREATER + + template optional_constexpr value_type value_or(U &&v) const optional_ref_qual { + return has_value() ? contained.value() : static_cast(std::forward(v)); + } + + template optional_constexpr14 value_type value_or(U &&v) optional_refref_qual { + return has_value() ? std::move(contained.value()) : static_cast(std::forward(v)); + } + +#else + + template optional_constexpr value_type value_or(U const &v) const { + return has_value() ? contained.value() : static_cast(v); + } + +#endif // optional_CPP11_OR_GREATER + + // x.x.3.6, modifiers + + void reset() optional_noexcept { + if (has_value()) { + contained.destruct_value(); + } + + has_value_ = false; + } + +private: + void this_type_does_not_support_comparisons() const {} + + template void initialize(V const &value) { + assert(!has_value()); + contained.construct_value(value); + has_value_ = true; + } + +#if optional_CPP11_OR_GREATER + template void initialize(V &&value) { + assert(!has_value()); + contained.construct_value(std::move(value)); + has_value_ = true; + } + +#endif + +private: + bool has_value_; + detail::storage_t contained; +}; + +// Relational operators + +template +inline optional_constexpr bool operator==(optional const &x, optional const &y) { + return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y; +} + +template +inline optional_constexpr bool operator!=(optional const &x, optional const &y) { + return !(x == y); +} + +template +inline optional_constexpr bool operator<(optional const &x, optional const &y) { + return (!y) ? false : (!x) ? true : *x < *y; +} + +template +inline optional_constexpr bool operator>(optional const &x, optional const &y) { + return (y < x); +} + +template +inline optional_constexpr bool operator<=(optional const &x, optional const &y) { + return !(y < x); +} + +template +inline optional_constexpr bool operator>=(optional const &x, optional const &y) { + return !(x < y); +} + +// Comparison with nullopt + +template +inline optional_constexpr bool operator==(optional const &x, + nullopt_t /*unused*/) optional_noexcept { + return (!x); +} + +template +inline optional_constexpr bool operator==(nullopt_t /*unused*/, + optional const &x) optional_noexcept { + return (!x); +} + +template +inline optional_constexpr bool operator!=(optional const &x, + nullopt_t /*unused*/) optional_noexcept { + return bool(x); +} + +template +inline optional_constexpr bool operator!=(nullopt_t /*unused*/, + optional const &x) optional_noexcept { + return bool(x); +} + +template +inline optional_constexpr bool operator<(optional const & /*unused*/, + nullopt_t /*unused*/) optional_noexcept { + return false; +} + +template +inline optional_constexpr bool operator<(nullopt_t /*unused*/, + optional const &x) optional_noexcept { + return bool(x); +} + +template +inline optional_constexpr bool operator<=(optional const &x, + nullopt_t /*unused*/) optional_noexcept { + return (!x); +} + +template +inline optional_constexpr bool operator<=(nullopt_t /*unused*/, + optional const & /*unused*/) optional_noexcept { + return true; +} + +template +inline optional_constexpr bool operator>(optional const &x, + nullopt_t /*unused*/) optional_noexcept { + return bool(x); +} + +template +inline optional_constexpr bool operator>(nullopt_t /*unused*/, + optional const & /*unused*/) optional_noexcept { + return false; +} + +template +inline optional_constexpr bool operator>=(optional const & /*unused*/, + nullopt_t /*unused*/) optional_noexcept { + return true; +} + +template +inline optional_constexpr bool operator>=(nullopt_t /*unused*/, + optional const &x) optional_noexcept { + return (!x); +} + +// Comparison with T + +template +inline optional_constexpr bool operator==(optional const &x, U const &v) { + return bool(x) ? *x == v : false; +} + +template +inline optional_constexpr bool operator==(U const &v, optional const &x) { + return bool(x) ? v == *x : false; +} + +template +inline optional_constexpr bool operator!=(optional const &x, U const &v) { + return bool(x) ? *x != v : true; +} + +template +inline optional_constexpr bool operator!=(U const &v, optional const &x) { + return bool(x) ? v != *x : true; +} + +template +inline optional_constexpr bool operator<(optional const &x, U const &v) { + return bool(x) ? *x < v : true; +} + +template +inline optional_constexpr bool operator<(U const &v, optional const &x) { + return bool(x) ? v < *x : false; +} + +template +inline optional_constexpr bool operator<=(optional const &x, U const &v) { + return bool(x) ? *x <= v : true; +} + +template +inline optional_constexpr bool operator<=(U const &v, optional const &x) { + return bool(x) ? v <= *x : false; +} + +template +inline optional_constexpr bool operator>(optional const &x, U const &v) { + return bool(x) ? *x > v : false; +} + +template +inline optional_constexpr bool operator>(U const &v, optional const &x) { + return bool(x) ? v > *x : true; +} + +template +inline optional_constexpr bool operator>=(optional const &x, U const &v) { + return bool(x) ? *x >= v : false; +} + +template +inline optional_constexpr bool operator>=(U const &v, optional const &x) { + return bool(x) ? v >= *x : true; +} + +// Specialized algorithms + +template < + typename T +#if optional_CPP11_OR_GREATER + optional_REQUIRES_T(std::is_move_constructible::value &&std17::is_swappable::value) +#endif + > +void swap(optional &x, optional &y) +#if optional_CPP11_OR_GREATER + noexcept(noexcept(x.swap(y))) +#endif +{ + x.swap(y); +} + +#if optional_CPP11_OR_GREATER + +template +optional_constexpr optional::type> make_optional(T &&value) { + return optional::type>(std::forward(value)); +} + +template +optional_constexpr optional make_optional(Args &&... args) { + return optional(nonstd_lite_in_place(T), std::forward(args)...); +} + +template +optional_constexpr optional make_optional(std::initializer_list il, Args &&... args) { + return optional(nonstd_lite_in_place(T), il, std::forward(args)...); +} + +#else + +template optional make_optional(T const &value) { return optional(value); } + +#endif // optional_CPP11_OR_GREATER + +} // namespace optional_lite + +using optional_lite::nullopt; +using optional_lite::nullopt_t; +using optional_lite::optional; + +#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 struct hash> { +public: + std::size_t operator()(nonstd::optional const &v) const optional_noexcept { + return bool(v) ? std::hash{}(*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 +// Copyright 2017-2020 by Martin Moene +// +// string-view lite, a C++17-like string_view for C++98 and later. +// For more information see https://github.com/martinmoene/string-view-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_SV_LITE_H_INCLUDED +#define NONSTD_SV_LITE_H_INCLUDED + +#define string_view_lite_MAJOR 1 +#define string_view_lite_MINOR 6 +#define string_view_lite_PATCH 0 + +#define string_view_lite_VERSION nssv_STRINGIFY(string_view_lite_MAJOR) "." nssv_STRINGIFY(string_view_lite_MINOR) "." nssv_STRINGIFY(string_view_lite_PATCH) + +#define nssv_STRINGIFY( x ) nssv_STRINGIFY_( x ) +#define nssv_STRINGIFY_( x ) #x + +// string-view lite configuration: + +#define nssv_STRING_VIEW_DEFAULT 0 +#define nssv_STRING_VIEW_NONSTD 1 +#define nssv_STRING_VIEW_STD 2 + +// tweak header support: + +#ifdef __has_include +# if __has_include() +# include +# endif +#define nssv_HAVE_TWEAK_HEADER 1 +#else +#define nssv_HAVE_TWEAK_HEADER 0 +//# pragma message("string_view.hpp: Note: Tweak header not supported.") +#endif + +// string_view selection and configuration: + +#if !defined( nssv_CONFIG_SELECT_STRING_VIEW ) +# define nssv_CONFIG_SELECT_STRING_VIEW ( nssv_HAVE_STD_STRING_VIEW ? nssv_STRING_VIEW_STD : nssv_STRING_VIEW_NONSTD ) +#endif + +#ifndef nssv_CONFIG_STD_SV_OPERATOR +# define nssv_CONFIG_STD_SV_OPERATOR 0 +#endif + +#ifndef nssv_CONFIG_USR_SV_OPERATOR +# define nssv_CONFIG_USR_SV_OPERATOR 1 +#endif + +#ifdef nssv_CONFIG_CONVERSION_STD_STRING +# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS nssv_CONFIG_CONVERSION_STD_STRING +# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS nssv_CONFIG_CONVERSION_STD_STRING +#endif + +#ifndef nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS +# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS 1 +#endif + +#ifndef nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS +# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS 1 +#endif + +#ifndef nssv_CONFIG_NO_STREAM_INSERTION +# define nssv_CONFIG_NO_STREAM_INSERTION 0 +#endif + +// Control presence of exception handling (try and auto discover): + +#ifndef nssv_CONFIG_NO_EXCEPTIONS +# if defined(_MSC_VER) +# include // for _HAS_EXCEPTIONS +# endif +# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS) +# define nssv_CONFIG_NO_EXCEPTIONS 0 +# else +# define nssv_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 nssv_CPLUSPLUS +# if defined(_MSVC_LANG ) && !defined(__clang__) +# define nssv_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) +# else +# define nssv_CPLUSPLUS __cplusplus +# endif +#endif + +#define nssv_CPP98_OR_GREATER ( nssv_CPLUSPLUS >= 199711L ) +#define nssv_CPP11_OR_GREATER ( nssv_CPLUSPLUS >= 201103L ) +#define nssv_CPP11_OR_GREATER_ ( nssv_CPLUSPLUS >= 201103L ) +#define nssv_CPP14_OR_GREATER ( nssv_CPLUSPLUS >= 201402L ) +#define nssv_CPP17_OR_GREATER ( nssv_CPLUSPLUS >= 201703L ) +#define nssv_CPP20_OR_GREATER ( nssv_CPLUSPLUS >= 202000L ) + +// use C++17 std::string_view if available and requested: + +#if nssv_CPP17_OR_GREATER && defined(__has_include ) +# if __has_include( ) +# define nssv_HAVE_STD_STRING_VIEW 1 +# else +# define nssv_HAVE_STD_STRING_VIEW 0 +# endif +#else +# define nssv_HAVE_STD_STRING_VIEW 0 +#endif + +#define nssv_USES_STD_STRING_VIEW ( (nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_STD) || ((nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_DEFAULT) && nssv_HAVE_STD_STRING_VIEW) ) + +#define nssv_HAVE_STARTS_WITH ( nssv_CPP20_OR_GREATER || !nssv_USES_STD_STRING_VIEW ) +#define nssv_HAVE_ENDS_WITH nssv_HAVE_STARTS_WITH + +// +// Use C++17 std::string_view: +// + +#if nssv_USES_STD_STRING_VIEW + +#include + +// Extensions for std::string: + +#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS + +namespace nonstd { + +template< class CharT, class Traits, class Allocator = std::allocator > +std::basic_string +to_string( std::basic_string_view v, Allocator const & a = Allocator() ) +{ + return std::basic_string( v.begin(), v.end(), a ); +} + +template< class CharT, class Traits, class Allocator > +std::basic_string_view +to_string_view( std::basic_string const & s ) +{ + return std::basic_string_view( s.data(), s.size() ); +} + +// Literal operators sv and _sv: + +#if nssv_CONFIG_STD_SV_OPERATOR + +using namespace std::literals::string_view_literals; + +#endif + +#if nssv_CONFIG_USR_SV_OPERATOR + +inline namespace literals { +inline namespace string_view_literals { + + +constexpr std::string_view operator "" _sv( const char* str, size_t len ) noexcept // (1) +{ + return std::string_view{ str, len }; +} + +constexpr std::u16string_view operator "" _sv( const char16_t* str, size_t len ) noexcept // (2) +{ + return std::u16string_view{ str, len }; +} + +constexpr std::u32string_view operator "" _sv( const char32_t* str, size_t len ) noexcept // (3) +{ + return std::u32string_view{ str, len }; +} + +constexpr std::wstring_view operator "" _sv( const wchar_t* str, size_t len ) noexcept // (4) +{ + return std::wstring_view{ str, len }; +} + +}} // namespace literals::string_view_literals + +#endif // nssv_CONFIG_USR_SV_OPERATOR + +} // namespace nonstd + +#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS + +namespace nonstd { + +using std::string_view; +using std::wstring_view; +using std::u16string_view; +using std::u32string_view; +using std::basic_string_view; + +// literal "sv" and "_sv", see above + +using std::operator==; +using std::operator!=; +using std::operator<; +using std::operator<=; +using std::operator>; +using std::operator>=; + +using std::operator<<; + +} // namespace nonstd + +#else // nssv_HAVE_STD_STRING_VIEW + +// +// Before C++17: use string_view lite: +// + +// Compiler versions: +// +// MSVC++ 6.0 _MSC_VER == 1200 nssv_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0) +// MSVC++ 7.0 _MSC_VER == 1300 nssv_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002) +// MSVC++ 7.1 _MSC_VER == 1310 nssv_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003) +// MSVC++ 8.0 _MSC_VER == 1400 nssv_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005) +// MSVC++ 9.0 _MSC_VER == 1500 nssv_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008) +// MSVC++ 10.0 _MSC_VER == 1600 nssv_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010) +// MSVC++ 11.0 _MSC_VER == 1700 nssv_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012) +// MSVC++ 12.0 _MSC_VER == 1800 nssv_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013) +// MSVC++ 14.0 _MSC_VER == 1900 nssv_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015) +// MSVC++ 14.1 _MSC_VER >= 1910 nssv_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017) +// MSVC++ 14.2 _MSC_VER >= 1920 nssv_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019) + +#if defined(_MSC_VER ) && !defined(__clang__) +# define nssv_COMPILER_MSVC_VER (_MSC_VER ) +# define nssv_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900 ) ) ) +#else +# define nssv_COMPILER_MSVC_VER 0 +# define nssv_COMPILER_MSVC_VERSION 0 +#endif + +#define nssv_COMPILER_VERSION( major, minor, patch ) ( 10 * ( 10 * (major) + (minor) ) + (patch) ) + +#if defined( __apple_build_version__ ) +# define nssv_COMPILER_APPLECLANG_VERSION nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +# define nssv_COMPILER_CLANG_VERSION 0 +#elif defined( __clang__ ) +# define nssv_COMPILER_APPLECLANG_VERSION 0 +# define nssv_COMPILER_CLANG_VERSION nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +#else +# define nssv_COMPILER_APPLECLANG_VERSION 0 +# define nssv_COMPILER_CLANG_VERSION 0 +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# define nssv_COMPILER_GNUC_VERSION nssv_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#else +# define nssv_COMPILER_GNUC_VERSION 0 +#endif + +// half-open range [lo..hi): +#define nssv_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) ) + +// Presence of language and library features: + +#ifdef _HAS_CPP0X +# define nssv_HAS_CPP0X _HAS_CPP0X +#else +# define nssv_HAS_CPP0X 0 +#endif + +// Unless defined otherwise below, consider VC14 as C++11 for variant-lite: + +#if nssv_COMPILER_MSVC_VER >= 1900 +# undef nssv_CPP11_OR_GREATER +# define nssv_CPP11_OR_GREATER 1 +#endif + +#define nssv_CPP11_90 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1500) +#define nssv_CPP11_100 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1600) +#define nssv_CPP11_110 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1700) +#define nssv_CPP11_120 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1800) +#define nssv_CPP11_140 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1900) +#define nssv_CPP11_141 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1910) + +#define nssv_CPP14_000 (nssv_CPP14_OR_GREATER) +#define nssv_CPP17_000 (nssv_CPP17_OR_GREATER) + +// Presence of C++11 language features: + +#define nssv_HAVE_CONSTEXPR_11 nssv_CPP11_140 +#define nssv_HAVE_EXPLICIT_CONVERSION nssv_CPP11_140 +#define nssv_HAVE_INLINE_NAMESPACE nssv_CPP11_140 +#define nssv_HAVE_NOEXCEPT nssv_CPP11_140 +#define nssv_HAVE_NULLPTR nssv_CPP11_100 +#define nssv_HAVE_REF_QUALIFIER nssv_CPP11_140 +#define nssv_HAVE_UNICODE_LITERALS nssv_CPP11_140 +#define nssv_HAVE_USER_DEFINED_LITERALS nssv_CPP11_140 +#define nssv_HAVE_WCHAR16_T nssv_CPP11_100 +#define nssv_HAVE_WCHAR32_T nssv_CPP11_100 + +#if ! ( ( nssv_CPP11_OR_GREATER && nssv_COMPILER_CLANG_VERSION ) || nssv_BETWEEN( nssv_COMPILER_CLANG_VERSION, 300, 400 ) ) +# define nssv_HAVE_STD_DEFINED_LITERALS nssv_CPP11_140 +#else +# define nssv_HAVE_STD_DEFINED_LITERALS 0 +#endif + +// Presence of C++14 language features: + +#define nssv_HAVE_CONSTEXPR_14 nssv_CPP14_000 + +// Presence of C++17 language features: + +#define nssv_HAVE_NODISCARD nssv_CPP17_000 + +// Presence of C++ library features: + +#define nssv_HAVE_STD_HASH nssv_CPP11_120 + +// Presence of compiler intrinsics: + +// Providing char-type specializations for compare() and length() that +// use compiler intrinsics can improve compile- and run-time performance. +// +// The challenge is in using the right combinations of builtin availability +// and its constexpr-ness. +// +// | compiler | __builtin_memcmp (constexpr) | memcmp (constexpr) | +// |----------|------------------------------|---------------------| +// | clang | 4.0 (>= 4.0 ) | any (? ) | +// | clang-a | 9.0 (>= 9.0 ) | any (? ) | +// | gcc | any (constexpr) | any (? ) | +// | msvc | >= 14.2 C++17 (>= 14.2 ) | any (? ) | + +#define nssv_HAVE_BUILTIN_VER ( (nssv_CPP17_000 && nssv_COMPILER_MSVC_VERSION >= 142) || nssv_COMPILER_GNUC_VERSION > 0 || nssv_COMPILER_CLANG_VERSION >= 400 || nssv_COMPILER_APPLECLANG_VERSION >= 900 ) +#define nssv_HAVE_BUILTIN_CE ( nssv_HAVE_BUILTIN_VER ) + +#define nssv_HAVE_BUILTIN_MEMCMP ( (nssv_HAVE_CONSTEXPR_14 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_14 ) +#define nssv_HAVE_BUILTIN_STRLEN ( (nssv_HAVE_CONSTEXPR_11 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_11 ) + +#ifdef __has_builtin +# define nssv_HAVE_BUILTIN( x ) __has_builtin( x ) +#else +# define nssv_HAVE_BUILTIN( x ) 0 +#endif + +#if nssv_HAVE_BUILTIN(__builtin_memcmp) || nssv_HAVE_BUILTIN_VER +# define nssv_BUILTIN_MEMCMP __builtin_memcmp +#else +# define nssv_BUILTIN_MEMCMP memcmp +#endif + +#if nssv_HAVE_BUILTIN(__builtin_strlen) || nssv_HAVE_BUILTIN_VER +# define nssv_BUILTIN_STRLEN __builtin_strlen +#else +# define nssv_BUILTIN_STRLEN strlen +#endif + +// C++ feature usage: + +#if nssv_HAVE_CONSTEXPR_11 +# define nssv_constexpr constexpr +#else +# define nssv_constexpr /*constexpr*/ +#endif + +#if nssv_HAVE_CONSTEXPR_14 +# define nssv_constexpr14 constexpr +#else +# define nssv_constexpr14 /*constexpr*/ +#endif + +#if nssv_HAVE_EXPLICIT_CONVERSION +# define nssv_explicit explicit +#else +# define nssv_explicit /*explicit*/ +#endif + +#if nssv_HAVE_INLINE_NAMESPACE +# define nssv_inline_ns inline +#else +# define nssv_inline_ns /*inline*/ +#endif + +#if nssv_HAVE_NOEXCEPT +# define nssv_noexcept noexcept +#else +# define nssv_noexcept /*noexcept*/ +#endif + +//#if nssv_HAVE_REF_QUALIFIER +//# define nssv_ref_qual & +//# define nssv_refref_qual && +//#else +//# define nssv_ref_qual /*&*/ +//# define nssv_refref_qual /*&&*/ +//#endif + +#if nssv_HAVE_NULLPTR +# define nssv_nullptr nullptr +#else +# define nssv_nullptr NULL +#endif + +#if nssv_HAVE_NODISCARD +# define nssv_nodiscard [[nodiscard]] +#else +# define nssv_nodiscard /*[[nodiscard]]*/ +#endif + +// Additional includes: + +#include +#include +#include +#include +#include // std::char_traits<> + +#if ! nssv_CONFIG_NO_STREAM_INSERTION +# include +#endif + +#if ! nssv_CONFIG_NO_EXCEPTIONS +# include +#endif + +#if nssv_CPP11_OR_GREATER +# include +#endif + +// Clang, GNUC, MSVC warning suppression macros: + +#if defined(__clang__) +# pragma clang diagnostic ignored "-Wreserved-user-defined-literal" +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wuser-defined-literals" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wliteral-suffix" +#endif // __clang__ + +#if nssv_COMPILER_MSVC_VERSION >= 140 +# define nssv_SUPPRESS_MSGSL_WARNING(expr) [[gsl::suppress(expr)]] +# define nssv_SUPPRESS_MSVC_WARNING(code, descr) __pragma(warning(suppress: code) ) +# define nssv_DISABLE_MSVC_WARNINGS(codes) __pragma(warning(push)) __pragma(warning(disable: codes)) +#else +# define nssv_SUPPRESS_MSGSL_WARNING(expr) +# define nssv_SUPPRESS_MSVC_WARNING(code, descr) +# define nssv_DISABLE_MSVC_WARNINGS(codes) +#endif + +#if defined(__clang__) +# define nssv_RESTORE_WARNINGS() _Pragma("clang diagnostic pop") +#elif defined(__GNUC__) +# define nssv_RESTORE_WARNINGS() _Pragma("GCC diagnostic pop") +#elif nssv_COMPILER_MSVC_VERSION >= 140 +# define nssv_RESTORE_WARNINGS() __pragma(warning(pop )) +#else +# define nssv_RESTORE_WARNINGS() +#endif + +// Suppress the following MSVC (GSL) warnings: +// - C4455, non-gsl : 'operator ""sv': literal suffix identifiers that do not +// start with an underscore are reserved +// - C26472, gsl::t.1 : don't use a static_cast for arithmetic conversions; +// use brace initialization, gsl::narrow_cast or gsl::narow +// - C26481: gsl::b.1 : don't use pointer arithmetic. Use span instead + +nssv_DISABLE_MSVC_WARNINGS( 4455 26481 26472 ) +//nssv_DISABLE_CLANG_WARNINGS( "-Wuser-defined-literals" ) +//nssv_DISABLE_GNUC_WARNINGS( -Wliteral-suffix ) + +namespace nonstd { namespace sv_lite { + +namespace detail { + +// support constexpr comparison in C++14; +// for C++17 and later, use provided traits: + +template< typename CharT > +inline nssv_constexpr14 int compare( CharT const * s1, CharT const * s2, std::size_t count ) +{ + while ( count-- != 0 ) + { + if ( *s1 < *s2 ) return -1; + if ( *s1 > *s2 ) return +1; + ++s1; ++s2; + } + return 0; +} + +#if nssv_HAVE_BUILTIN_MEMCMP + +// specialization of compare() for char, see also generic compare() above: + +inline nssv_constexpr14 int compare( char const * s1, char const * s2, std::size_t count ) +{ + return nssv_BUILTIN_MEMCMP( s1, s2, count ); +} + +#endif + +#if nssv_HAVE_BUILTIN_STRLEN + +// specialization of length() for char, see also generic length() further below: + +inline nssv_constexpr std::size_t length( char const * s ) +{ + return nssv_BUILTIN_STRLEN( s ); +} + +#endif + +#if defined(__OPTIMIZE__) + +// gcc, clang provide __OPTIMIZE__ +// Expect tail call optimization to make length() non-recursive: + +template< typename CharT > +inline nssv_constexpr std::size_t length( CharT * s, std::size_t result = 0 ) +{ + return *s == '\0' ? result : length( s + 1, result + 1 ); +} + +#else // OPTIMIZE + +// non-recursive: + +template< typename CharT > +inline nssv_constexpr14 std::size_t length( CharT * s ) +{ + std::size_t result = 0; + while ( *s++ != '\0' ) + { + ++result; + } + return result; +} + +#endif // OPTIMIZE + +} // namespace detail + +template +< + class CharT, + class Traits = std::char_traits +> +class basic_string_view; + +// +// basic_string_view: +// + +template +< + class CharT, + class Traits /* = std::char_traits */ +> +class basic_string_view +{ +public: + // Member types: + + typedef Traits traits_type; + typedef CharT value_type; + + typedef CharT * pointer; + typedef CharT const * const_pointer; + typedef CharT & reference; + typedef CharT const & const_reference; + + typedef const_pointer iterator; + typedef const_pointer const_iterator; + typedef std::reverse_iterator< const_iterator > reverse_iterator; + typedef std::reverse_iterator< const_iterator > const_reverse_iterator; + + typedef std::size_t size_type; + typedef std::ptrdiff_t difference_type; + + // 24.4.2.1 Construction and assignment: + + nssv_constexpr basic_string_view() nssv_noexcept + : data_( nssv_nullptr ) + , size_( 0 ) + {} + +#if nssv_CPP11_OR_GREATER + nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept = default; +#else + nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept + : data_( other.data_) + , size_( other.size_) + {} +#endif + + nssv_constexpr basic_string_view( CharT const * s, size_type count ) nssv_noexcept // non-standard noexcept + : data_( s ) + , size_( count ) + {} + + nssv_constexpr basic_string_view( CharT const * s) nssv_noexcept // non-standard noexcept + : data_( s ) +#if nssv_CPP17_OR_GREATER + , size_( Traits::length(s) ) +#elif nssv_CPP11_OR_GREATER + , size_( detail::length(s) ) +#else + , size_( Traits::length(s) ) +#endif + {} + + // Assignment: + +#if nssv_CPP11_OR_GREATER + nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept = default; +#else + nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept + { + data_ = other.data_; + size_ = other.size_; + return *this; + } +#endif + + // 24.4.2.2 Iterator support: + + nssv_constexpr const_iterator begin() const nssv_noexcept { return data_; } + nssv_constexpr const_iterator end() const nssv_noexcept { return data_ + size_; } + + nssv_constexpr const_iterator cbegin() const nssv_noexcept { return begin(); } + nssv_constexpr const_iterator cend() const nssv_noexcept { return end(); } + + nssv_constexpr const_reverse_iterator rbegin() const nssv_noexcept { return const_reverse_iterator( end() ); } + nssv_constexpr const_reverse_iterator rend() const nssv_noexcept { return const_reverse_iterator( begin() ); } + + nssv_constexpr const_reverse_iterator crbegin() const nssv_noexcept { return rbegin(); } + nssv_constexpr const_reverse_iterator crend() const nssv_noexcept { return rend(); } + + // 24.4.2.3 Capacity: + + nssv_constexpr size_type size() const nssv_noexcept { return size_; } + nssv_constexpr size_type length() const nssv_noexcept { return size_; } + nssv_constexpr size_type max_size() const nssv_noexcept { return (std::numeric_limits< size_type >::max)(); } + + // since C++20 + nssv_nodiscard nssv_constexpr bool empty() const nssv_noexcept + { + return 0 == size_; + } + + // 24.4.2.4 Element access: + + nssv_constexpr const_reference operator[]( size_type pos ) const + { + return data_at( pos ); + } + + nssv_constexpr14 const_reference at( size_type pos ) const + { +#if nssv_CONFIG_NO_EXCEPTIONS + assert( pos < size() ); +#else + if ( pos >= size() ) + { + throw std::out_of_range("nonstd::string_view::at()"); + } +#endif + return data_at( pos ); + } + + nssv_constexpr const_reference front() const { return data_at( 0 ); } + nssv_constexpr const_reference back() const { return data_at( size() - 1 ); } + + nssv_constexpr const_pointer data() const nssv_noexcept { return data_; } + + // 24.4.2.5 Modifiers: + + nssv_constexpr14 void remove_prefix( size_type n ) + { + assert( n <= size() ); + data_ += n; + size_ -= n; + } + + nssv_constexpr14 void remove_suffix( size_type n ) + { + assert( n <= size() ); + size_ -= n; + } + + nssv_constexpr14 void swap( basic_string_view & other ) nssv_noexcept + { + const basic_string_view tmp(other); + other = *this; + *this = tmp; + } + + // 24.4.2.6 String operations: + + size_type copy( CharT * dest, size_type n, size_type pos = 0 ) const + { +#if nssv_CONFIG_NO_EXCEPTIONS + assert( pos <= size() ); +#else + if ( pos > size() ) + { + throw std::out_of_range("nonstd::string_view::copy()"); + } +#endif + const size_type rlen = (std::min)( n, size() - pos ); + + (void) Traits::copy( dest, data() + pos, rlen ); + + return rlen; + } + + nssv_constexpr14 basic_string_view substr( size_type pos = 0, size_type n = npos ) const + { +#if nssv_CONFIG_NO_EXCEPTIONS + assert( pos <= size() ); +#else + if ( pos > size() ) + { + throw std::out_of_range("nonstd::string_view::substr()"); + } +#endif + return basic_string_view( data() + pos, (std::min)( n, size() - pos ) ); + } + + // compare(), 6x: + + nssv_constexpr14 int compare( basic_string_view other ) const nssv_noexcept // (1) + { +#if nssv_CPP17_OR_GREATER + if ( const int result = Traits::compare( data(), other.data(), (std::min)( size(), other.size() ) ) ) +#else + if ( const int result = detail::compare( data(), other.data(), (std::min)( size(), other.size() ) ) ) +#endif + { + return result; + } + + return size() == other.size() ? 0 : size() < other.size() ? -1 : 1; + } + + nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other ) const // (2) + { + return substr( pos1, n1 ).compare( other ); + } + + nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other, size_type pos2, size_type n2 ) const // (3) + { + return substr( pos1, n1 ).compare( other.substr( pos2, n2 ) ); + } + + nssv_constexpr int compare( CharT const * s ) const // (4) + { + return compare( basic_string_view( s ) ); + } + + nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s ) const // (5) + { + return substr( pos1, n1 ).compare( basic_string_view( s ) ); + } + + nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s, size_type n2 ) const // (6) + { + return substr( pos1, n1 ).compare( basic_string_view( s, n2 ) ); + } + + // 24.4.2.7 Searching: + + // starts_with(), 3x, since C++20: + + nssv_constexpr bool starts_with( basic_string_view v ) const nssv_noexcept // (1) + { + return size() >= v.size() && compare( 0, v.size(), v ) == 0; + } + + nssv_constexpr bool starts_with( CharT c ) const nssv_noexcept // (2) + { + return starts_with( basic_string_view( &c, 1 ) ); + } + + nssv_constexpr bool starts_with( CharT const * s ) const // (3) + { + return starts_with( basic_string_view( s ) ); + } + + // ends_with(), 3x, since C++20: + + nssv_constexpr bool ends_with( basic_string_view v ) const nssv_noexcept // (1) + { + return size() >= v.size() && compare( size() - v.size(), npos, v ) == 0; + } + + nssv_constexpr bool ends_with( CharT c ) const nssv_noexcept // (2) + { + return ends_with( basic_string_view( &c, 1 ) ); + } + + nssv_constexpr bool ends_with( CharT const * s ) const // (3) + { + return ends_with( basic_string_view( s ) ); + } + + // find(), 4x: + + nssv_constexpr14 size_type find( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1) + { + return assert( v.size() == 0 || v.data() != nssv_nullptr ) + , pos >= size() + ? npos + : to_pos( std::search( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) ); + } + + nssv_constexpr14 size_type find( CharT c, size_type pos = 0 ) const nssv_noexcept // (2) + { + return find( basic_string_view( &c, 1 ), pos ); + } + + nssv_constexpr14 size_type find( CharT const * s, size_type pos, size_type n ) const // (3) + { + return find( basic_string_view( s, n ), pos ); + } + + nssv_constexpr14 size_type find( CharT const * s, size_type pos = 0 ) const // (4) + { + return find( basic_string_view( s ), pos ); + } + + // rfind(), 4x: + + nssv_constexpr14 size_type rfind( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1) + { + if ( size() < v.size() ) + { + return npos; + } + + if ( v.empty() ) + { + return (std::min)( size(), pos ); + } + + const_iterator last = cbegin() + (std::min)( size() - v.size(), pos ) + v.size(); + const_iterator result = std::find_end( cbegin(), last, v.cbegin(), v.cend(), Traits::eq ); + + return result != last ? size_type( result - cbegin() ) : npos; + } + + nssv_constexpr14 size_type rfind( CharT c, size_type pos = npos ) const nssv_noexcept // (2) + { + return rfind( basic_string_view( &c, 1 ), pos ); + } + + nssv_constexpr14 size_type rfind( CharT const * s, size_type pos, size_type n ) const // (3) + { + return rfind( basic_string_view( s, n ), pos ); + } + + nssv_constexpr14 size_type rfind( CharT const * s, size_type pos = npos ) const // (4) + { + return rfind( basic_string_view( s ), pos ); + } + + // find_first_of(), 4x: + + nssv_constexpr size_type find_first_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1) + { + return pos >= size() + ? npos + : to_pos( std::find_first_of( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) ); + } + + nssv_constexpr size_type find_first_of( CharT c, size_type pos = 0 ) const nssv_noexcept // (2) + { + return find_first_of( basic_string_view( &c, 1 ), pos ); + } + + nssv_constexpr size_type find_first_of( CharT const * s, size_type pos, size_type n ) const // (3) + { + return find_first_of( basic_string_view( s, n ), pos ); + } + + nssv_constexpr size_type find_first_of( CharT const * s, size_type pos = 0 ) const // (4) + { + return find_first_of( basic_string_view( s ), pos ); + } + + // find_last_of(), 4x: + + nssv_constexpr size_type find_last_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1) + { + return empty() + ? npos + : pos >= size() + ? find_last_of( v, size() - 1 ) + : to_pos( std::find_first_of( const_reverse_iterator( cbegin() + pos + 1 ), crend(), v.cbegin(), v.cend(), Traits::eq ) ); + } + + nssv_constexpr size_type find_last_of( CharT c, size_type pos = npos ) const nssv_noexcept // (2) + { + return find_last_of( basic_string_view( &c, 1 ), pos ); + } + + nssv_constexpr size_type find_last_of( CharT const * s, size_type pos, size_type count ) const // (3) + { + return find_last_of( basic_string_view( s, count ), pos ); + } + + nssv_constexpr size_type find_last_of( CharT const * s, size_type pos = npos ) const // (4) + { + return find_last_of( basic_string_view( s ), pos ); + } + + // find_first_not_of(), 4x: + + nssv_constexpr size_type find_first_not_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1) + { + return pos >= size() + ? npos + : to_pos( std::find_if( cbegin() + pos, cend(), not_in_view( v ) ) ); + } + + nssv_constexpr size_type find_first_not_of( CharT c, size_type pos = 0 ) const nssv_noexcept // (2) + { + return find_first_not_of( basic_string_view( &c, 1 ), pos ); + } + + nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos, size_type count ) const // (3) + { + return find_first_not_of( basic_string_view( s, count ), pos ); + } + + nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos = 0 ) const // (4) + { + return find_first_not_of( basic_string_view( s ), pos ); + } + + // find_last_not_of(), 4x: + + nssv_constexpr size_type find_last_not_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1) + { + return empty() + ? npos + : pos >= size() + ? find_last_not_of( v, size() - 1 ) + : to_pos( std::find_if( const_reverse_iterator( cbegin() + pos + 1 ), crend(), not_in_view( v ) ) ); + } + + nssv_constexpr size_type find_last_not_of( CharT c, size_type pos = npos ) const nssv_noexcept // (2) + { + return find_last_not_of( basic_string_view( &c, 1 ), pos ); + } + + nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos, size_type count ) const // (3) + { + return find_last_not_of( basic_string_view( s, count ), pos ); + } + + nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos = npos ) const // (4) + { + return find_last_not_of( basic_string_view( s ), pos ); + } + + // Constants: + +#if nssv_CPP17_OR_GREATER + static nssv_constexpr size_type npos = size_type(-1); +#elif nssv_CPP11_OR_GREATER + enum : size_type { npos = size_type(-1) }; +#else + enum { npos = size_type(-1) }; +#endif + +private: + struct not_in_view + { + const basic_string_view v; + + nssv_constexpr explicit not_in_view( basic_string_view v_ ) : v( v_ ) {} + + nssv_constexpr bool operator()( CharT c ) const + { + return npos == v.find_first_of( c ); + } + }; + + nssv_constexpr size_type to_pos( const_iterator it ) const + { + return it == cend() ? npos : size_type( it - cbegin() ); + } + + nssv_constexpr size_type to_pos( const_reverse_iterator it ) const + { + return it == crend() ? npos : size_type( crend() - it - 1 ); + } + + nssv_constexpr const_reference data_at( size_type pos ) const + { +#if nssv_BETWEEN( nssv_COMPILER_GNUC_VERSION, 1, 500 ) + return data_[pos]; +#else + return assert( pos < size() ), data_[pos]; +#endif + } + +private: + const_pointer data_; + size_type size_; + +public: +#if nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS + + template< class Allocator > + basic_string_view( std::basic_string const & s ) nssv_noexcept + : data_( s.data() ) + , size_( s.size() ) + {} + +#if nssv_HAVE_EXPLICIT_CONVERSION + + template< class Allocator > + explicit operator std::basic_string() const + { + return to_string( Allocator() ); + } + +#endif // nssv_HAVE_EXPLICIT_CONVERSION + +#if nssv_CPP11_OR_GREATER + + template< class Allocator = std::allocator > + std::basic_string + to_string( Allocator const & a = Allocator() ) const + { + return std::basic_string( begin(), end(), a ); + } + +#else + + std::basic_string + to_string() const + { + return std::basic_string( begin(), end() ); + } + + template< class Allocator > + std::basic_string + to_string( Allocator const & a ) const + { + return std::basic_string( begin(), end(), a ); + } + +#endif // nssv_CPP11_OR_GREATER + +#endif // nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS +}; + +// +// Non-member functions: +// + +// 24.4.3 Non-member comparison functions: +// lexicographically compare two string views (function template): + +template< class CharT, class Traits > +nssv_constexpr bool operator== ( + basic_string_view lhs, + basic_string_view rhs ) nssv_noexcept +{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; } + +template< class CharT, class Traits > +nssv_constexpr bool operator!= ( + basic_string_view lhs, + basic_string_view rhs ) nssv_noexcept +{ return !( lhs == rhs ); } + +template< class CharT, class Traits > +nssv_constexpr bool operator< ( + basic_string_view lhs, + basic_string_view rhs ) nssv_noexcept +{ return lhs.compare( rhs ) < 0; } + +template< class CharT, class Traits > +nssv_constexpr bool operator<= ( + basic_string_view lhs, + basic_string_view rhs ) nssv_noexcept +{ return lhs.compare( rhs ) <= 0; } + +template< class CharT, class Traits > +nssv_constexpr bool operator> ( + basic_string_view lhs, + basic_string_view rhs ) nssv_noexcept +{ return lhs.compare( rhs ) > 0; } + +template< class CharT, class Traits > +nssv_constexpr bool operator>= ( + basic_string_view lhs, + basic_string_view rhs ) nssv_noexcept +{ return lhs.compare( rhs ) >= 0; } + +// Let S be basic_string_view, and sv be an instance of S. +// Implementations shall provide sufficient additional overloads marked +// constexpr and noexcept so that an object t with an implicit conversion +// to S can be compared according to Table 67. + +#if ! nssv_CPP11_OR_GREATER || nssv_BETWEEN( nssv_COMPILER_MSVC_VERSION, 100, 141 ) + +// accommodate for older compilers: + +// == + +template< class CharT, class Traits> +nssv_constexpr bool operator==( + basic_string_view lhs, + CharT const * rhs ) nssv_noexcept +{ return lhs.size() == detail::length( rhs ) && lhs.compare( rhs ) == 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator==( + CharT const * lhs, + basic_string_view rhs ) nssv_noexcept +{ return detail::length( lhs ) == rhs.size() && rhs.compare( lhs ) == 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator==( + basic_string_view lhs, + std::basic_string rhs ) nssv_noexcept +{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator==( + std::basic_string rhs, + basic_string_view lhs ) nssv_noexcept +{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; } + +// != + +template< class CharT, class Traits> +nssv_constexpr bool operator!=( + basic_string_view lhs, + CharT const * rhs ) nssv_noexcept +{ return !( lhs == rhs ); } + +template< class CharT, class Traits> +nssv_constexpr bool operator!=( + CharT const * lhs, + basic_string_view rhs ) nssv_noexcept +{ return !( lhs == rhs ); } + +template< class CharT, class Traits> +nssv_constexpr bool operator!=( + basic_string_view lhs, + std::basic_string rhs ) nssv_noexcept +{ return !( lhs == rhs ); } + +template< class CharT, class Traits> +nssv_constexpr bool operator!=( + std::basic_string rhs, + basic_string_view lhs ) nssv_noexcept +{ return !( lhs == rhs ); } + +// < + +template< class CharT, class Traits> +nssv_constexpr bool operator<( + basic_string_view lhs, + CharT const * rhs ) nssv_noexcept +{ return lhs.compare( rhs ) < 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator<( + CharT const * lhs, + basic_string_view rhs ) nssv_noexcept +{ return rhs.compare( lhs ) > 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator<( + basic_string_view lhs, + std::basic_string rhs ) nssv_noexcept +{ return lhs.compare( rhs ) < 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator<( + std::basic_string rhs, + basic_string_view lhs ) nssv_noexcept +{ return rhs.compare( lhs ) > 0; } + +// <= + +template< class CharT, class Traits> +nssv_constexpr bool operator<=( + basic_string_view lhs, + CharT const * rhs ) nssv_noexcept +{ return lhs.compare( rhs ) <= 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator<=( + CharT const * lhs, + basic_string_view rhs ) nssv_noexcept +{ return rhs.compare( lhs ) >= 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator<=( + basic_string_view lhs, + std::basic_string rhs ) nssv_noexcept +{ return lhs.compare( rhs ) <= 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator<=( + std::basic_string rhs, + basic_string_view lhs ) nssv_noexcept +{ return rhs.compare( lhs ) >= 0; } + +// > + +template< class CharT, class Traits> +nssv_constexpr bool operator>( + basic_string_view lhs, + CharT const * rhs ) nssv_noexcept +{ return lhs.compare( rhs ) > 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator>( + CharT const * lhs, + basic_string_view rhs ) nssv_noexcept +{ return rhs.compare( lhs ) < 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator>( + basic_string_view lhs, + std::basic_string rhs ) nssv_noexcept +{ return lhs.compare( rhs ) > 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator>( + std::basic_string rhs, + basic_string_view lhs ) nssv_noexcept +{ return rhs.compare( lhs ) < 0; } + +// >= + +template< class CharT, class Traits> +nssv_constexpr bool operator>=( + basic_string_view lhs, + CharT const * rhs ) nssv_noexcept +{ return lhs.compare( rhs ) >= 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator>=( + CharT const * lhs, + basic_string_view rhs ) nssv_noexcept +{ return rhs.compare( lhs ) <= 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator>=( + basic_string_view lhs, + std::basic_string rhs ) nssv_noexcept +{ return lhs.compare( rhs ) >= 0; } + +template< class CharT, class Traits> +nssv_constexpr bool operator>=( + std::basic_string rhs, + basic_string_view lhs ) nssv_noexcept +{ return rhs.compare( lhs ) <= 0; } + +#else // newer compilers: + +#define nssv_BASIC_STRING_VIEW_I(T,U) typename std::decay< basic_string_view >::type + +#if defined(_MSC_VER) // issue 40 +# define nssv_MSVC_ORDER(x) , int=x +#else +# define nssv_MSVC_ORDER(x) /*, int=x*/ +#endif + +// == + +template< class CharT, class Traits nssv_MSVC_ORDER(1) > +nssv_constexpr bool operator==( + basic_string_view lhs, + nssv_BASIC_STRING_VIEW_I(CharT, Traits) rhs ) nssv_noexcept +{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; } + +template< class CharT, class Traits nssv_MSVC_ORDER(2) > +nssv_constexpr bool operator==( + nssv_BASIC_STRING_VIEW_I(CharT, Traits) lhs, + basic_string_view rhs ) nssv_noexcept +{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; } + +// != + +template< class CharT, class Traits nssv_MSVC_ORDER(1) > +nssv_constexpr bool operator!= ( + basic_string_view < CharT, Traits > lhs, + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept +{ return !( lhs == rhs ); } + +template< class CharT, class Traits nssv_MSVC_ORDER(2) > +nssv_constexpr bool operator!= ( + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs, + basic_string_view < CharT, Traits > rhs ) nssv_noexcept +{ return !( lhs == rhs ); } + +// < + +template< class CharT, class Traits nssv_MSVC_ORDER(1) > +nssv_constexpr bool operator< ( + basic_string_view < CharT, Traits > lhs, + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept +{ return lhs.compare( rhs ) < 0; } + +template< class CharT, class Traits nssv_MSVC_ORDER(2) > +nssv_constexpr bool operator< ( + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs, + basic_string_view < CharT, Traits > rhs ) nssv_noexcept +{ return lhs.compare( rhs ) < 0; } + +// <= + +template< class CharT, class Traits nssv_MSVC_ORDER(1) > +nssv_constexpr bool operator<= ( + basic_string_view < CharT, Traits > lhs, + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept +{ return lhs.compare( rhs ) <= 0; } + +template< class CharT, class Traits nssv_MSVC_ORDER(2) > +nssv_constexpr bool operator<= ( + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs, + basic_string_view < CharT, Traits > rhs ) nssv_noexcept +{ return lhs.compare( rhs ) <= 0; } + +// > + +template< class CharT, class Traits nssv_MSVC_ORDER(1) > +nssv_constexpr bool operator> ( + basic_string_view < CharT, Traits > lhs, + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept +{ return lhs.compare( rhs ) > 0; } + +template< class CharT, class Traits nssv_MSVC_ORDER(2) > +nssv_constexpr bool operator> ( + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs, + basic_string_view < CharT, Traits > rhs ) nssv_noexcept +{ return lhs.compare( rhs ) > 0; } + +// >= + +template< class CharT, class Traits nssv_MSVC_ORDER(1) > +nssv_constexpr bool operator>= ( + basic_string_view < CharT, Traits > lhs, + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept +{ return lhs.compare( rhs ) >= 0; } + +template< class CharT, class Traits nssv_MSVC_ORDER(2) > +nssv_constexpr bool operator>= ( + nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs, + basic_string_view < CharT, Traits > rhs ) nssv_noexcept +{ return lhs.compare( rhs ) >= 0; } + +#undef nssv_MSVC_ORDER +#undef nssv_BASIC_STRING_VIEW_I + +#endif // compiler-dependent approach to comparisons + +// 24.4.4 Inserters and extractors: + +#if ! nssv_CONFIG_NO_STREAM_INSERTION + +namespace detail { + +template< class Stream > +void write_padding( Stream & os, std::streamsize n ) +{ + for ( std::streamsize i = 0; i < n; ++i ) + os.rdbuf()->sputc( os.fill() ); +} + +template< class Stream, class View > +Stream & write_to_stream( Stream & os, View const & sv ) +{ + typename Stream::sentry sentry( os ); + + if ( !os ) + return os; + + const std::streamsize length = static_cast( sv.length() ); + + // Whether, and how, to pad: + const bool pad = ( length < os.width() ); + const bool left_pad = pad && ( os.flags() & std::ios_base::adjustfield ) == std::ios_base::right; + + if ( left_pad ) + write_padding( os, os.width() - length ); + + // Write span characters: + os.rdbuf()->sputn( sv.begin(), length ); + + if ( pad && !left_pad ) + write_padding( os, os.width() - length ); + + // Reset output stream width: + os.width( 0 ); + + return os; +} + +} // namespace detail + +template< class CharT, class Traits > +std::basic_ostream & +operator<<( + std::basic_ostream& os, + basic_string_view sv ) +{ + return detail::write_to_stream( os, sv ); +} + +#endif // nssv_CONFIG_NO_STREAM_INSERTION + +// Several typedefs for common character types are provided: + +typedef basic_string_view string_view; +typedef basic_string_view wstring_view; +#if nssv_HAVE_WCHAR16_T +typedef basic_string_view u16string_view; +typedef basic_string_view u32string_view; +#endif + +}} // namespace nonstd::sv_lite + +// +// 24.4.6 Suffix for basic_string_view literals: +// + +#if nssv_HAVE_USER_DEFINED_LITERALS + +namespace nonstd { +nssv_inline_ns namespace literals { +nssv_inline_ns namespace string_view_literals { + +#if nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS + +nssv_constexpr nonstd::sv_lite::string_view operator "" sv( const char* str, size_t len ) nssv_noexcept // (1) +{ + return nonstd::sv_lite::string_view{ str, len }; +} + +nssv_constexpr nonstd::sv_lite::u16string_view operator "" sv( const char16_t* str, size_t len ) nssv_noexcept // (2) +{ + return nonstd::sv_lite::u16string_view{ str, len }; +} + +nssv_constexpr nonstd::sv_lite::u32string_view operator "" sv( const char32_t* str, size_t len ) nssv_noexcept // (3) +{ + return nonstd::sv_lite::u32string_view{ str, len }; +} + +nssv_constexpr nonstd::sv_lite::wstring_view operator "" sv( const wchar_t* str, size_t len ) nssv_noexcept // (4) +{ + return nonstd::sv_lite::wstring_view{ str, len }; +} + +#endif // nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS + +#if nssv_CONFIG_USR_SV_OPERATOR + +nssv_constexpr nonstd::sv_lite::string_view operator "" _sv( const char* str, size_t len ) nssv_noexcept // (1) +{ + return nonstd::sv_lite::string_view{ str, len }; +} + +nssv_constexpr nonstd::sv_lite::u16string_view operator "" _sv( const char16_t* str, size_t len ) nssv_noexcept // (2) +{ + return nonstd::sv_lite::u16string_view{ str, len }; +} + +nssv_constexpr nonstd::sv_lite::u32string_view operator "" _sv( const char32_t* str, size_t len ) nssv_noexcept // (3) +{ + return nonstd::sv_lite::u32string_view{ str, len }; +} + +nssv_constexpr nonstd::sv_lite::wstring_view operator "" _sv( const wchar_t* str, size_t len ) nssv_noexcept // (4) +{ + return nonstd::sv_lite::wstring_view{ str, len }; +} + +#endif // nssv_CONFIG_USR_SV_OPERATOR + +}}} // namespace nonstd::literals::string_view_literals + +#endif + +// +// Extensions for std::string: +// + +#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS + +namespace nonstd { +namespace sv_lite { + +// Exclude MSVC 14 (19.00): it yields ambiguous to_string(): + +#if nssv_CPP11_OR_GREATER && nssv_COMPILER_MSVC_VERSION != 140 + +template< class CharT, class Traits, class Allocator = std::allocator > +std::basic_string +to_string( basic_string_view v, Allocator const & a = Allocator() ) +{ + return std::basic_string( v.begin(), v.end(), a ); +} + +#else + +template< class CharT, class Traits > +std::basic_string +to_string( basic_string_view v ) +{ + return std::basic_string( v.begin(), v.end() ); +} + +template< class CharT, class Traits, class Allocator > +std::basic_string +to_string( basic_string_view v, Allocator const & a ) +{ + return std::basic_string( v.begin(), v.end(), a ); +} + +#endif // nssv_CPP11_OR_GREATER + +template< class CharT, class Traits, class Allocator > +basic_string_view +to_string_view( std::basic_string const & s ) +{ + return basic_string_view( s.data(), s.size() ); +} + +}} // namespace nonstd::sv_lite + +#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS + +// +// make types and algorithms available in namespace nonstd: +// + +namespace nonstd { + +using sv_lite::basic_string_view; +using sv_lite::string_view; +using sv_lite::wstring_view; + +#if nssv_HAVE_WCHAR16_T +using sv_lite::u16string_view; +#endif +#if nssv_HAVE_WCHAR32_T +using sv_lite::u32string_view; +#endif + +// literal "sv" + +using sv_lite::operator==; +using sv_lite::operator!=; +using sv_lite::operator<; +using sv_lite::operator<=; +using sv_lite::operator>; +using sv_lite::operator>=; + +#if ! nssv_CONFIG_NO_STREAM_INSERTION +using sv_lite::operator<<; +#endif + +#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS +using sv_lite::to_string; +using sv_lite::to_string_view; +#endif + +} // namespace nonstd + +// 24.4.5 Hash support (C++11): + +// Note: The hash value of a string view object is equal to the hash value of +// the corresponding string object. + +#if nssv_HAVE_STD_HASH + +#include + +namespace std { + +template<> +struct hash< nonstd::string_view > +{ +public: + std::size_t operator()( nonstd::string_view v ) const nssv_noexcept + { + return std::hash()( std::string( v.data(), v.size() ) ); + } +}; + +template<> +struct hash< nonstd::wstring_view > +{ +public: + std::size_t operator()( nonstd::wstring_view v ) const nssv_noexcept + { + return std::hash()( std::wstring( v.data(), v.size() ) ); + } +}; + +template<> +struct hash< nonstd::u16string_view > +{ +public: + std::size_t operator()( nonstd::u16string_view v ) const nssv_noexcept + { + return std::hash()( std::u16string( v.data(), v.size() ) ); + } +}; + +template<> +struct hash< nonstd::u32string_view > +{ +public: + std::size_t operator()( nonstd::u32string_view v ) const nssv_noexcept + { + return std::hash()( std::u32string( v.data(), v.size() ) ); + } +}; + +} // namespace std + +#endif // nssv_HAVE_STD_HASH + +nssv_RESTORE_WARNINGS() + +#endif // nssv_HAVE_STD_STRING_VIEW +#endif // NONSTD_SV_LITE_H_INCLUDED +//! +//! termcolor +//! ~~~~~~~~~ +//! +//! termcolor is a header-only c++ library for printing colored messages +//! to the terminal. Written just for fun with a help of the Force. +//! +//! :copyright: (c) 2013 by Ihor Kalnytskyi +//! :license: BSD, see LICENSE for details +//! + +#ifndef TERMCOLOR_HPP_ +#define TERMCOLOR_HPP_ + +// the following snippet of code detects the current OS and +// defines the appropriate macro that is used to wrap some +// platform specific things +#if defined(_WIN32) || defined(_WIN64) +#define TERMCOLOR_OS_WINDOWS +#elif defined(__APPLE__) +#define TERMCOLOR_OS_MACOS +#elif defined(__unix__) || defined(__unix) +#define TERMCOLOR_OS_LINUX +#else +#error unsupported platform +#endif + +// This headers provides the `isatty()`/`fileno()` functions, +// which are used for testing whether a standart stream refers +// to the terminal. As for Windows, we also need WinApi funcs +// for changing colors attributes of the terminal. +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) +#include +#elif defined(TERMCOLOR_OS_WINDOWS) +#include +#include +#endif + +#include +#include + +namespace termcolor { +// Forward declaration of the `_internal` namespace. +// All comments are below. +namespace _internal { +// An index to be used to access a private storage of I/O streams. See +// colorize / nocolorize I/O manipulators for details. +static int colorize_index = std::ios_base::xalloc(); + +inline FILE *get_standard_stream(const std::ostream &stream); +inline bool is_colorized(std::ostream &stream); +inline bool is_atty(const std::ostream &stream); + +#if defined(TERMCOLOR_OS_WINDOWS) +inline void win_change_attributes(std::ostream &stream, int foreground, int background = -1); +#endif +} // namespace _internal + +inline std::ostream &colorize(std::ostream &stream) { + stream.iword(_internal::colorize_index) = 1L; + return stream; +} + +inline std::ostream &nocolorize(std::ostream &stream) { + stream.iword(_internal::colorize_index) = 0L; + return stream; +} + +inline std::ostream &reset(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[00m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, -1); +#endif + } + return stream; +} + +inline std::ostream &bold(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[1m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &dark(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[2m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &italic(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[3m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &underline(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[4m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &blink(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[5m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &reverse(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[7m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &concealed(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[8m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &crossed(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[9m"; +#elif defined(TERMCOLOR_OS_WINDOWS) +#endif + } + return stream; +} + +inline std::ostream &grey(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[30m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + 0 // grey (black) + ); +#endif + } + return stream; +} + +inline std::ostream &red(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[31m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, FOREGROUND_RED); +#endif + } + return stream; +} + +inline std::ostream &green(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[32m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, FOREGROUND_GREEN); +#endif + } + return stream; +} + +inline std::ostream &yellow(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[33m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, FOREGROUND_GREEN | FOREGROUND_RED); +#endif + } + return stream; +} + +inline std::ostream &blue(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[34m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, FOREGROUND_BLUE); +#endif + } + return stream; +} + +inline std::ostream &magenta(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[35m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_RED); +#endif + } + return stream; +} + +inline std::ostream &cyan(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[36m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN); +#endif + } + return stream; +} + +inline std::ostream &white(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[37m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED); +#endif + } + return stream; +} + +inline std::ostream &on_grey(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[40m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + 0 // grey (black) + ); +#endif + } + return stream; +} + +inline std::ostream &on_red(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[41m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, BACKGROUND_RED); +#endif + } + return stream; +} + +inline std::ostream &on_green(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[42m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN); +#endif + } + return stream; +} + +inline std::ostream &on_yellow(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[43m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_RED); +#endif + } + return stream; +} + +inline std::ostream &on_blue(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[44m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE); +#endif + } + return stream; +} + +inline std::ostream &on_magenta(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[45m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE | BACKGROUND_RED); +#endif + } + return stream; +} + +inline std::ostream &on_cyan(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[46m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_BLUE); +#endif + } + return stream; +} + +inline std::ostream &on_white(std::ostream &stream) { + if (_internal::is_colorized(stream)) { +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[47m"; +#elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_RED); +#endif + } + + return stream; +} + +//! Since C++ hasn't a way to hide something in the header from +//! the outer access, I have to introduce this namespace which +//! is used for internal purpose and should't be access from +//! the user code. +namespace _internal { +//! Since C++ hasn't a true way to extract stream handler +//! from the a given `std::ostream` object, I have to write +//! this kind of hack. +inline FILE *get_standard_stream(const std::ostream &stream) { + if (&stream == &std::cout) + return stdout; + else if ((&stream == &std::cerr) || (&stream == &std::clog)) + return stderr; + + return 0; +} + +// Say whether a given stream should be colorized or not. It's always +// true for ATTY streams and may be true for streams marked with +// colorize flag. +inline bool is_colorized(std::ostream &stream) { + return is_atty(stream) || static_cast(stream.iword(colorize_index)); +} + +//! Test whether a given `std::ostream` object refers to +//! a terminal. +inline bool is_atty(const std::ostream &stream) { + FILE *std_stream = get_standard_stream(stream); + + // Unfortunately, fileno() ends with segmentation fault + // if invalid file descriptor is passed. So we need to + // handle this case gracefully and assume it's not a tty + // if standard stream is not detected, and 0 is returned. + if (!std_stream) + return false; + +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + return ::isatty(fileno(std_stream)); +#elif defined(TERMCOLOR_OS_WINDOWS) + return ::_isatty(_fileno(std_stream)); +#endif +} + +#if defined(TERMCOLOR_OS_WINDOWS) +//! Change Windows Terminal colors attribute. If some +//! parameter is `-1` then attribute won't changed. +inline void win_change_attributes(std::ostream &stream, int foreground, int background) { + // yeah, i know.. it's ugly, it's windows. + static WORD defaultAttributes = 0; + + // Windows doesn't have ANSI escape sequences and so we use special + // API to change Terminal output color. That means we can't + // manipulate colors by means of "std::stringstream" and hence + // should do nothing in this case. + if (!_internal::is_atty(stream)) + return; + + // get terminal handle + HANDLE hTerminal = INVALID_HANDLE_VALUE; + if (&stream == &std::cout) + hTerminal = GetStdHandle(STD_OUTPUT_HANDLE); + else if (&stream == &std::cerr) + hTerminal = GetStdHandle(STD_ERROR_HANDLE); + + // save default terminal attributes if it unsaved + if (!defaultAttributes) { + CONSOLE_SCREEN_BUFFER_INFO info; + if (!GetConsoleScreenBufferInfo(hTerminal, &info)) + return; + defaultAttributes = info.wAttributes; + } + + // restore all default settings + if (foreground == -1 && background == -1) { + SetConsoleTextAttribute(hTerminal, defaultAttributes); + return; + } + + // get current settings + CONSOLE_SCREEN_BUFFER_INFO info; + if (!GetConsoleScreenBufferInfo(hTerminal, &info)) + return; + + if (foreground != -1) { + info.wAttributes &= ~(info.wAttributes & 0x0F); + info.wAttributes |= static_cast(foreground); + } + + if (background != -1) { + info.wAttributes &= ~(info.wAttributes & 0xF0); + info.wAttributes |= static_cast(background); + } + + SetConsoleTextAttribute(hTerminal, info.wAttributes); +} +#endif // TERMCOLOR_OS_WINDOWS + +} // namespace _internal + +} // namespace termcolor + +#undef TERMCOLOR_OS_WINDOWS +#undef TERMCOLOR_OS_MACOS +#undef TERMCOLOR_OS_LINUX + +#endif // TERMCOLOR_HPP_ + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +#include +#include + +#include +#include + +#include +// #include +#include + +namespace tabulate { + +#if defined(__unix__) || defined(__unix) || defined(__APPLE__) +inline int get_wcswidth(const std::string &string, const std::string &locale, + size_t max_column_width) { + if (string.size() == 0) + return 0; + + // The behavior of wcswidth() depends on the LC_CTYPE category of the current locale. + // Set the current locale based on cell properties before computing width + auto old_locale = std::locale::global(std::locale(locale)); + + // Convert from narrow std::string to wide string + wchar_t *wide_string = new wchar_t[string.size()]; + std::mbstowcs(wide_string, string.c_str(), string.size()); + + // Compute display width of wide string + int result = wcswidth(wide_string, max_column_width); + delete[] wide_string; + + // Restore old locale + std::locale::global(old_locale); + + return result; +} +#endif + +inline size_t get_sequence_length(const std::string &text, const std::string &locale, + bool is_multi_byte_character_support_enabled) { + if (!is_multi_byte_character_support_enabled) + return text.length(); + +#if defined(_WIN32) || defined(_WIN64) + return (text.length() - std::count_if(text.begin(), text.end(), + [](char c) -> bool { return (c & 0xC0) == 0x80; })); +#elif defined(__unix__) || defined(__unix) || defined(__APPLE__) + auto result = get_wcswidth(text, locale, text.size()); + if (result >= 0) + return result; + else + return (text.length() - std::count_if(text.begin(), text.end(), + [](char c) -> bool { return (c & 0xC0) == 0x80; })); +#endif +} + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 + +namespace tabulate { + +enum class Color { none, grey, red, green, yellow, blue, magenta, cyan, white }; +} + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 + +namespace tabulate { + +enum class FontAlign { left, right, center }; +} + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 + +namespace tabulate { + +enum class FontStyle { bold, dark, italic, underline, blink, reverse, concealed, crossed }; +} + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +#include +#include +// #include +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +#include +#include +#include +#include +// #include +// #include +// #include +// #include + +#if __cplusplus >= 201703L +#include +using std::optional; +#else +// #include +using nonstd::optional; +#endif + +#include + +namespace tabulate { + +class Format { +public: + Format &width(size_t value) { + width_ = value; + return *this; + } + + Format &height(size_t value) { + height_ = value; + return *this; + } + + Format &padding(size_t value) { + padding_left_ = value; + padding_right_ = value; + padding_top_ = value; + padding_bottom_ = value; + return *this; + } + + Format &padding_left(size_t value) { + padding_left_ = value; + return *this; + } + + Format &padding_right(size_t value) { + padding_right_ = value; + return *this; + } + + Format &padding_top(size_t value) { + padding_top_ = value; + return *this; + } + + Format &padding_bottom(size_t value) { + padding_bottom_ = value; + return *this; + } + + Format &border(const std::string &value) { + border_left_ = value; + border_right_ = value; + border_top_ = value; + border_bottom_ = value; + return *this; + } + + Format &border_color(Color value) { + border_left_color_ = value; + border_right_color_ = value; + border_top_color_ = value; + border_bottom_color_ = value; + return *this; + } + + Format &border_background_color(Color value) { + border_left_background_color_ = value; + border_right_background_color_ = value; + border_top_background_color_ = value; + border_bottom_background_color_ = value; + return *this; + } + + Format &border_left(const std::string &value) { + border_left_ = value; + return *this; + } + + Format &border_left_color(Color value) { + border_left_color_ = value; + return *this; + } + + Format &border_left_background_color(Color value) { + border_left_background_color_ = value; + return *this; + } + + Format &border_right(const std::string &value) { + border_right_ = value; + return *this; + } + + Format &border_right_color(Color value) { + border_right_color_ = value; + return *this; + } + + Format &border_right_background_color(Color value) { + border_right_background_color_ = value; + return *this; + } + + Format &border_top(const std::string &value) { + border_top_ = value; + return *this; + } + + Format &border_top_color(Color value) { + border_top_color_ = value; + return *this; + } + + Format &border_top_background_color(Color value) { + border_top_background_color_ = value; + return *this; + } + + Format &border_bottom(const std::string &value) { + border_bottom_ = value; + return *this; + } + + Format &border_bottom_color(Color value) { + border_bottom_color_ = value; + return *this; + } + + Format &border_bottom_background_color(Color value) { + border_bottom_background_color_ = value; + return *this; + } + + Format &show_border() { + show_border_top_ = true; + show_border_bottom_ = true; + show_border_left_ = true; + show_border_right_ = true; + return *this; + } + + Format &hide_border() { + show_border_top_ = false; + show_border_bottom_ = false; + show_border_left_ = false; + show_border_right_ = false; + return *this; + } + + Format &show_border_top() { + show_border_top_ = true; + return *this; + } + + Format &hide_border_top() { + show_border_top_ = false; + return *this; + } + + Format &show_border_bottom() { + show_border_bottom_ = true; + return *this; + } + + Format &hide_border_bottom() { + show_border_bottom_ = false; + return *this; + } + + Format &show_border_left() { + show_border_left_ = true; + return *this; + } + + Format &hide_border_left() { + show_border_left_ = false; + return *this; + } + + Format &show_border_right() { + show_border_right_ = true; + return *this; + } + + Format &hide_border_right() { + show_border_right_ = false; + return *this; + } + + Format &corner(const std::string &value) { + corner_top_left_ = value; + corner_top_right_ = value; + corner_bottom_left_ = value; + corner_bottom_right_ = value; + return *this; + } + + Format &corner_color(Color value) { + corner_top_left_color_ = value; + corner_top_right_color_ = value; + corner_bottom_left_color_ = value; + corner_bottom_right_color_ = value; + return *this; + } + + Format &corner_background_color(Color value) { + corner_top_left_background_color_ = value; + corner_top_right_background_color_ = value; + corner_bottom_left_background_color_ = value; + corner_bottom_right_background_color_ = value; + return *this; + } + + Format &corner_top_left(const std::string &value) { + corner_top_left_ = value; + return *this; + } + + Format &corner_top_left_color(Color value) { + corner_top_left_color_ = value; + return *this; + } + + Format &corner_top_left_background_color(Color value) { + corner_top_left_background_color_ = value; + return *this; + } + + Format &corner_top_right(const std::string &value) { + corner_top_right_ = value; + return *this; + } + + Format &corner_top_right_color(Color value) { + corner_top_right_color_ = value; + return *this; + } + + Format &corner_top_right_background_color(Color value) { + corner_top_right_background_color_ = value; + return *this; + } + + Format &corner_bottom_left(const std::string &value) { + corner_bottom_left_ = value; + return *this; + } + + Format &corner_bottom_left_color(Color value) { + corner_bottom_left_color_ = value; + return *this; + } + + Format &corner_bottom_left_background_color(Color value) { + corner_bottom_left_background_color_ = value; + return *this; + } + + Format &corner_bottom_right(const std::string &value) { + corner_bottom_right_ = value; + return *this; + } + + Format &corner_bottom_right_color(Color value) { + corner_bottom_right_color_ = value; + return *this; + } + + Format &corner_bottom_right_background_color(Color value) { + corner_bottom_right_background_color_ = value; + return *this; + } + + Format &column_separator(const std::string &value) { + column_separator_ = value; + return *this; + } + + Format &column_separator_color(Color value) { + column_separator_color_ = value; + return *this; + } + + Format &column_separator_background_color(Color value) { + column_separator_background_color_ = value; + return *this; + } + + Format &font_align(FontAlign value) { + font_align_ = value; + return *this; + } + + Format &font_style(const std::vector &style) { + if (font_style_.has_value()) { + for (auto &s : style) + font_style_->push_back(s); + } else { + font_style_ = style; + } + return *this; + } + + Format &font_color(Color value) { + font_color_ = value; + return *this; + } + + Format &font_background_color(Color value) { + font_background_color_ = value; + return *this; + } + + Format &color(Color value) { + font_color(value); + border_color(value); + corner_color(value); + return *this; + } + + Format &background_color(Color value) { + font_background_color(value); + border_background_color(value); + corner_background_color(value); + return *this; + } + + Format &multi_byte_characters(bool value) { + multi_byte_characters_ = value; + return *this; + } + + Format &locale(const std::string &value) { + locale_ = value; + return *this; + } + + // Apply word wrap + // Given an input string and a line length, this will insert \n + // in strategic places in input string and apply word wrapping + static std::string word_wrap(const std::string &str, size_t width, const std::string &locale, + bool is_multi_byte_character_support_enabled) { + std::vector words = explode_string(str, {" ", "-", "\t"}); + size_t current_line_length = 0; + std::string result; + + for (size_t i = 0; i < words.size(); ++i) { + std::string word = words[i]; + // If adding the new word to the current line would be too long, + // then put it on a new line (and split it up if it's too long). + if (current_line_length + + get_sequence_length(word, locale, is_multi_byte_character_support_enabled) > + width) { + // Only move down to a new line if we have text on the current line. + // Avoids situation where wrapped whitespace causes emptylines in text. + if (current_line_length > 0) { + result += '\n'; + current_line_length = 0; + } + + // If the current word is too long to fit on a line even on it's own then + // split the word up. + while (get_sequence_length(word, locale, is_multi_byte_character_support_enabled) > width) { + result += word.substr(0, width - 1) + "-"; + word = word.substr(width - 1); + result += '\n'; + } + + // Remove leading whitespace from the word so the new line starts flush to the left. + word = trim_left(word); + } + result += word; + current_line_length += + get_sequence_length(word, locale, is_multi_byte_character_support_enabled); + } + return result; + } + + static std::vector split_lines(const std::string &text, const std::string &delimiter, + const std::string &locale, + bool is_multi_byte_character_support_enabled) { + std::vector result{}; + std::string input = text; + size_t pos = 0; + std::string token; + while ((pos = input.find(delimiter)) != std::string::npos) { + token = input.substr(0, pos); + result.push_back(token); + input.erase(0, pos + delimiter.length()); + } + if (get_sequence_length(input, locale, is_multi_byte_character_support_enabled)) + result.push_back(input); + return result; + }; + + // Merge two formats + // first has higher precedence + // e.g., first = cell-level formatting and + // second = row-level formatting + // Result has attributes of both with cell-level + // formatting taking precedence + static Format merge(Format first, Format second) { + Format result; + + // Width and height + if (first.width_.has_value()) + result.width_ = first.width_; + else + result.width_ = second.width_; + + if (first.height_.has_value()) + result.height_ = first.height_; + else + result.height_ = second.height_; + + // Font styling + if (first.font_align_.has_value()) + result.font_align_ = first.font_align_; + else + result.font_align_ = second.font_align_; + + if (first.font_style_.has_value()) { + // Merge font styles using std::set_union + std::vector merged_font_style(first.font_style_->size() + + second.font_style_->size()); +#if defined(_WIN32) || defined(_WIN64) + // Fixes error in Windows - Sequence not ordered + std::sort(first.font_style_->begin(), first.font_style_->end()); + std::sort(second.font_style_->begin(), second.font_style_->end()); +#endif + std::set_union(first.font_style_->begin(), first.font_style_->end(), + second.font_style_->begin(), second.font_style_->end(), + merged_font_style.begin()); + result.font_style_ = merged_font_style; + } else + result.font_style_ = second.font_style_; + + if (first.font_color_.has_value()) + result.font_color_ = first.font_color_; + else + result.font_color_ = second.font_color_; + + if (first.font_background_color_.has_value()) + result.font_background_color_ = first.font_background_color_; + else + result.font_background_color_ = second.font_background_color_; + + // Padding + if (first.padding_left_.has_value()) + result.padding_left_ = first.padding_left_; + else + result.padding_left_ = second.padding_left_; + + if (first.padding_top_.has_value()) + result.padding_top_ = first.padding_top_; + else + result.padding_top_ = second.padding_top_; + + if (first.padding_right_.has_value()) + result.padding_right_ = first.padding_right_; + else + result.padding_right_ = second.padding_right_; + + if (first.padding_bottom_.has_value()) + result.padding_bottom_ = first.padding_bottom_; + else + result.padding_bottom_ = second.padding_bottom_; + + // Border + if (first.border_left_.has_value()) + result.border_left_ = first.border_left_; + else + result.border_left_ = second.border_left_; + + if (first.border_left_color_.has_value()) + result.border_left_color_ = first.border_left_color_; + else + result.border_left_color_ = second.border_left_color_; + + if (first.border_left_background_color_.has_value()) + result.border_left_background_color_ = first.border_left_background_color_; + else + result.border_left_background_color_ = second.border_left_background_color_; + + if (first.border_top_.has_value()) + result.border_top_ = first.border_top_; + else + result.border_top_ = second.border_top_; + + if (first.border_top_color_.has_value()) + result.border_top_color_ = first.border_top_color_; + else + result.border_top_color_ = second.border_top_color_; + + if (first.border_top_background_color_.has_value()) + result.border_top_background_color_ = first.border_top_background_color_; + else + result.border_top_background_color_ = second.border_top_background_color_; + + if (first.border_bottom_.has_value()) + result.border_bottom_ = first.border_bottom_; + else + result.border_bottom_ = second.border_bottom_; + + if (first.border_bottom_color_.has_value()) + result.border_bottom_color_ = first.border_bottom_color_; + else + result.border_bottom_color_ = second.border_bottom_color_; + + if (first.border_bottom_background_color_.has_value()) + result.border_bottom_background_color_ = first.border_bottom_background_color_; + else + result.border_bottom_background_color_ = second.border_bottom_background_color_; + + if (first.border_right_.has_value()) + result.border_right_ = first.border_right_; + else + result.border_right_ = second.border_right_; + + if (first.border_right_color_.has_value()) + result.border_right_color_ = first.border_right_color_; + else + result.border_right_color_ = second.border_right_color_; + + if (first.border_right_background_color_.has_value()) + result.border_right_background_color_ = first.border_right_background_color_; + else + result.border_right_background_color_ = second.border_right_background_color_; + + if (first.show_border_top_.has_value()) + result.show_border_top_ = first.show_border_top_; + else + result.show_border_top_ = second.show_border_top_; + + if (first.show_border_bottom_.has_value()) + result.show_border_bottom_ = first.show_border_bottom_; + else + result.show_border_bottom_ = second.show_border_bottom_; + + if (first.show_border_left_.has_value()) + result.show_border_left_ = first.show_border_left_; + else + result.show_border_left_ = second.show_border_left_; + + if (first.show_border_right_.has_value()) + result.show_border_right_ = first.show_border_right_; + else + result.show_border_right_ = second.show_border_right_; + + // Corner + if (first.corner_top_left_.has_value()) + result.corner_top_left_ = first.corner_top_left_; + else + result.corner_top_left_ = second.corner_top_left_; + + if (first.corner_top_left_color_.has_value()) + result.corner_top_left_color_ = first.corner_top_left_color_; + else + result.corner_top_left_color_ = second.corner_top_left_color_; + + if (first.corner_top_left_background_color_.has_value()) + result.corner_top_left_background_color_ = first.corner_top_left_background_color_; + else + result.corner_top_left_background_color_ = second.corner_top_left_background_color_; + + if (first.corner_top_right_.has_value()) + result.corner_top_right_ = first.corner_top_right_; + else + result.corner_top_right_ = second.corner_top_right_; + + if (first.corner_top_right_color_.has_value()) + result.corner_top_right_color_ = first.corner_top_right_color_; + else + result.corner_top_right_color_ = second.corner_top_right_color_; + + if (first.corner_top_right_background_color_.has_value()) + result.corner_top_right_background_color_ = first.corner_top_right_background_color_; + else + result.corner_top_right_background_color_ = second.corner_top_right_background_color_; + + if (first.corner_bottom_left_.has_value()) + result.corner_bottom_left_ = first.corner_bottom_left_; + else + result.corner_bottom_left_ = second.corner_bottom_left_; + + if (first.corner_bottom_left_color_.has_value()) + result.corner_bottom_left_color_ = first.corner_bottom_left_color_; + else + result.corner_bottom_left_color_ = second.corner_bottom_left_color_; + + if (first.corner_bottom_left_background_color_.has_value()) + result.corner_bottom_left_background_color_ = first.corner_bottom_left_background_color_; + else + result.corner_bottom_left_background_color_ = second.corner_bottom_left_background_color_; + + if (first.corner_bottom_right_.has_value()) + result.corner_bottom_right_ = first.corner_bottom_right_; + else + result.corner_bottom_right_ = second.corner_bottom_right_; + + if (first.corner_bottom_right_color_.has_value()) + result.corner_bottom_right_color_ = first.corner_bottom_right_color_; + else + result.corner_bottom_right_color_ = second.corner_bottom_right_color_; + + if (first.corner_bottom_right_background_color_.has_value()) + result.corner_bottom_right_background_color_ = first.corner_bottom_right_background_color_; + else + result.corner_bottom_right_background_color_ = second.corner_bottom_right_background_color_; + + // Column separator + if (first.column_separator_.has_value()) + result.column_separator_ = first.column_separator_; + else + result.column_separator_ = second.column_separator_; + + if (first.column_separator_color_.has_value()) + result.column_separator_color_ = first.column_separator_color_; + else + result.column_separator_color_ = second.column_separator_color_; + + if (first.column_separator_background_color_.has_value()) + result.column_separator_background_color_ = first.column_separator_background_color_; + else + result.column_separator_background_color_ = second.column_separator_background_color_; + + // Internationlization + if (first.multi_byte_characters_.has_value()) + result.multi_byte_characters_ = first.multi_byte_characters_; + else + result.multi_byte_characters_ = second.multi_byte_characters_; + + if (first.locale_.has_value()) + result.locale_ = first.locale_; + else + result.locale_ = second.locale_; + + return result; + } + +private: + friend class Cell; + friend class Row; + friend class Column; + friend class TableInternal; + friend class Printer; + friend class MarkdownExporter; + friend class LatexExporter; + friend class AsciiDocExporter; + + void set_defaults() { + // NOTE: width and height are not set here + font_align_ = FontAlign::left; + font_style_ = std::vector{}; + font_color_ = font_background_color_ = Color::none; + padding_left_ = padding_right_ = 1; + padding_top_ = padding_bottom_ = 0; + border_top_ = border_bottom_ = "-"; + border_left_ = border_right_ = "|"; + show_border_left_ = show_border_right_ = show_border_top_ = show_border_bottom_ = true; + border_top_color_ = border_top_background_color_ = border_bottom_color_ = + border_bottom_background_color_ = border_left_color_ = border_left_background_color_ = + border_right_color_ = border_right_background_color_ = Color::none; + corner_top_left_ = corner_top_right_ = corner_bottom_left_ = corner_bottom_right_ = "+"; + corner_top_left_color_ = corner_top_left_background_color_ = corner_top_right_color_ = + corner_top_right_background_color_ = corner_bottom_left_color_ = + corner_bottom_left_background_color_ = corner_bottom_right_color_ = + corner_bottom_right_background_color_ = Color::none; + column_separator_ = "|"; + column_separator_color_ = column_separator_background_color_ = Color::none; + multi_byte_characters_ = false; + locale_ = ""; + } + + // Helper methods for word wrapping: + + // trim white spaces from the left end of an input string + static std::string trim_left(const std::string &input_string) { + std::string result = input_string; + result.erase(result.begin(), std::find_if(result.begin(), result.end(), + [](int ch) { return !std::isspace(ch); })); + return result; + } + + // trim white spaces from right end of an input string + static std::string trim_right(const std::string &input_string) { + std::string result = input_string; + result.erase( + std::find_if(result.rbegin(), result.rend(), [](int ch) { return !std::isspace(ch); }) + .base(), + result.end()); + return result; + } + + // trim white spaces from either end of an input string + static std::string trim(const std::string &input_string) { + return trim_left(trim_right(input_string)); + } + + static size_t index_of_any(const std::string &input, size_t start_index, + const std::vector &split_characters) { + std::vector indices{}; + for (auto &c : split_characters) { + auto index = input.find(c, start_index); + if (index != std::string::npos) + indices.push_back(index); + } + if (indices.size() > 0) + return *std::min_element(indices.begin(), indices.end()); + else + return std::string::npos; + } + + static std::vector explode_string(const std::string &input, + const std::vector &split_characters) { + std::vector result{}; + size_t start_index{0}; + while (true) { + auto index = index_of_any(input, start_index, split_characters); + + if (index == std::string::npos) { + result.push_back(input.substr(start_index)); + return result; + } + + std::string word = input.substr(start_index, index - start_index); + char next_character = input.substr(index, 1)[0]; + // Unlike whitespace, dashes and the like should stick to the word occurring before it. + if (isspace(next_character)) { + result.push_back(word); + result.push_back(std::string(1, next_character)); + } else { + result.push_back(word + next_character); + } + start_index = index + 1; + } + + return result; + } + + // Element width and height + optional width_{}; + optional height_{}; + + // Font styling + optional font_align_{}; + optional> font_style_{}; + optional font_color_{}; + optional font_background_color_{}; + + // Element padding + optional padding_left_{}; + optional padding_top_{}; + optional padding_right_{}; + optional padding_bottom_{}; + + // Element border + optional show_border_top_{}; + optional border_top_{}; + optional border_top_color_{}; + optional border_top_background_color_{}; + + optional show_border_bottom_{}; + optional border_bottom_{}; + optional border_bottom_color_{}; + optional border_bottom_background_color_{}; + + optional show_border_left_{}; + optional border_left_{}; + optional border_left_color_{}; + optional border_left_background_color_{}; + + optional show_border_right_{}; + optional border_right_{}; + optional border_right_color_{}; + optional border_right_background_color_{}; + + // Element corner + optional corner_top_left_{}; + optional corner_top_left_color_{}; + optional corner_top_left_background_color_{}; + + optional corner_top_right_{}; + optional corner_top_right_color_{}; + optional corner_top_right_background_color_{}; + + optional corner_bottom_left_{}; + optional corner_bottom_left_color_{}; + optional corner_bottom_left_background_color_{}; + + optional corner_bottom_right_{}; + optional corner_bottom_right_color_{}; + optional corner_bottom_right_background_color_{}; + + // Element column separator + optional column_separator_{}; + optional column_separator_color_{}; + optional column_separator_background_color_{}; + + // Internationalization + optional multi_byte_characters_{}; + optional locale_{}; +}; + +} // namespace tabulate + +// #include + +#if __cplusplus >= 201703L +#include +using std::optional; +#else +// #include +using nonstd::optional; +#endif + +#include + +namespace tabulate { + +class Cell { +public: + explicit Cell(std::shared_ptr parent) : parent_(parent) {} + + void set_text(const std::string &text) { data_ = text; } + + const std::string &get_text() { return data_; } + + size_t size() { + return get_sequence_length(data_, locale(), is_multi_byte_character_support_enabled()); + } + + std::string locale() { return *format().locale_; } + + Format &format(); + + bool is_multi_byte_character_support_enabled(); + +private: + std::string data_; + std::weak_ptr parent_; + optional format_; +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +#include +#include +// #include + +#if __cplusplus >= 201703L +#include +using std::optional; +#else +// #include +using nonstd::optional; +#endif + +#include +#ifdef max +#undef max +#endif +#ifdef min +#undef min +#endif + +namespace tabulate { + +class Row { +public: + explicit Row(std::shared_ptr parent) : parent_(parent) {} + + void add_cell(std::shared_ptr cell) { cells_.push_back(cell); } + + Cell &operator[](size_t index) { return cell(index); } + + Cell &cell(size_t index) { return *(cells_[index]); } + + std::vector> cells() const { return cells_; } + + size_t size() const { return cells_.size(); } + + Format &format(); + + class CellIterator { + public: + explicit CellIterator(std::vector>::iterator ptr) : ptr(ptr) {} + + CellIterator operator++() { + ++ptr; + return *this; + } + bool operator!=(const CellIterator &other) const { return ptr != other.ptr; } + Cell &operator*() { return **ptr; } + + private: + std::vector>::iterator ptr; + }; + + auto begin() -> CellIterator { return CellIterator(cells_.begin()); } + auto end() -> CellIterator { return CellIterator(cells_.end()); } + +private: + friend class Printer; + + // Returns the row height as configured + // For each cell in the row, check the cell.format.height + // property and return the largest configured row height + // This is used to ensure that all cells in a row are + // aligned when printing the column + size_t get_configured_height() { + size_t result{0}; + for (size_t i = 0; i < size(); ++i) { + auto cell = cells_[i]; + auto format = cell->format(); + if (format.height_.has_value()) + result = std::max(result, *format.height_); + } + return result; + } + + // Computes the height of the row based on cell contents + // and configured cell padding + // For each cell, compute: + // padding_top + (cell_contents / column height) + padding_bottom + // and return the largest value + // + // This is useful when no cell.format.height is configured + // Call get_configured_height() + // - If this returns 0, then use get_computed_height() + size_t get_computed_height(const std::vector &column_widths) { + size_t result{0}; + for (size_t i = 0; i < size(); ++i) { + result = std::max(result, get_cell_height(i, column_widths[i])); + } + return result; + } + + // Returns padding_top + cell_contents / column_height + padding_bottom + // for a given cell in the column + // e.g., + // column width = 5 + // cell_contents = "I love tabulate" (size/length = 15) + // padding top and padding bottom are 1 + // then, cell height = 1 + (15 / 5) + 1 = 1 + 3 + 1 = 5 + // The cell will look like this: + // + // ..... + // I lov + // e tab + // ulate + // ..... + size_t get_cell_height(size_t cell_index, size_t column_width) { + size_t result{0}; + Cell &cell = *(cells_[cell_index]); + auto format = cell.format(); + auto text = cell.get_text(); + + auto padding_left = *format.padding_left_; + auto padding_right = *format.padding_right_; + + result += *format.padding_top_; + + if (column_width > (padding_left + padding_right)) { + column_width -= (padding_left + padding_right); + } + + // Check if input text has embedded newline characters + auto newlines_in_text = std::count(text.begin(), text.end(), '\n'); + std::string word_wrapped_text; + if (newlines_in_text == 0) { + // No new lines in input + // Apply automatic word wrapping and compute row height + word_wrapped_text = Format::word_wrap(text, column_width, cell.locale(), + cell.is_multi_byte_character_support_enabled()); + } else { + // There are embedded '\n' characters + // Respect these characters + word_wrapped_text = text; + } + + auto newlines_in_wrapped_text = + std::count(word_wrapped_text.begin(), word_wrapped_text.end(), '\n'); + auto estimated_row_height = newlines_in_wrapped_text; + + if (!word_wrapped_text.empty() && + word_wrapped_text[word_wrapped_text.size() - 1] != '\n') // text doesn't end with a newline + estimated_row_height += 1; + + result += estimated_row_height; + + result += *format.padding_bottom_; + + return result; + } + + std::vector> cells_; + std::weak_ptr parent_; + optional format_; +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 + +namespace tabulate { + +class ColumnFormat : public Format { +public: + explicit ColumnFormat(class Column &column) : column_(column) {} + + ColumnFormat &width(size_t value); + ColumnFormat &height(size_t value); + + // Padding + ColumnFormat &padding(size_t value); + ColumnFormat &padding_left(size_t value); + ColumnFormat &padding_right(size_t value); + ColumnFormat &padding_top(size_t value); + ColumnFormat &padding_bottom(size_t value); + + // Border + ColumnFormat &border(const std::string &value); + ColumnFormat &border_color(Color value); + ColumnFormat &border_background_color(Color value); + ColumnFormat &border_left(const std::string &value); + ColumnFormat &border_left_color(Color value); + ColumnFormat &border_left_background_color(Color value); + ColumnFormat &border_right(const std::string &value); + ColumnFormat &border_right_color(Color value); + ColumnFormat &border_right_background_color(Color value); + ColumnFormat &border_top(const std::string &value); + ColumnFormat &border_top_color(Color value); + ColumnFormat &border_top_background_color(Color value); + ColumnFormat &border_bottom(const std::string &value); + ColumnFormat &border_bottom_color(Color value); + ColumnFormat &border_bottom_background_color(Color value); + + // Corner + ColumnFormat &corner(const std::string &value); + ColumnFormat &corner_color(Color value); + ColumnFormat &corner_background_color(Color value); + + // Column separator + ColumnFormat &column_separator(const std::string &value); + ColumnFormat &column_separator_color(Color value); + ColumnFormat &column_separator_background_color(Color value); + + // Font styling + ColumnFormat &font_align(FontAlign value); + ColumnFormat &font_style(const std::vector &style); + ColumnFormat &font_color(Color value); + ColumnFormat &font_background_color(Color value); + ColumnFormat &color(Color value); + ColumnFormat &background_color(Color value); + + // Locale + ColumnFormat &multi_byte_characters(bool value); + ColumnFormat &locale(const std::string &value); + +private: + std::reference_wrapper column_; +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +#include +#include +#include +#include +#include +// #include +// #include +#include +#ifdef max +#undef max +#endif +#ifdef min +#undef min +#endif + +namespace tabulate { + +class Column { +public: + explicit Column(std::shared_ptr parent) : parent_(parent) {} + + void add_cell(Cell &cell) { cells_.push_back(cell); } + + Cell &operator[](size_t index) { return cells_[index]; } + + std::vector> cells() const { return cells_; } + + size_t size() const { return cells_.size(); } + + ColumnFormat format() { return ColumnFormat(*this); } + + class CellIterator { + public: + explicit CellIterator(std::vector>::iterator ptr) : ptr(ptr) {} + + CellIterator operator++() { + ++ptr; + return *this; + } + bool operator!=(const CellIterator &other) const { return ptr != other.ptr; } + Cell &operator*() { return *ptr; } + + private: + std::vector>::iterator ptr; + }; + + auto begin() -> CellIterator { return CellIterator(cells_.begin()); } + auto end() -> CellIterator { return CellIterator(cells_.end()); } + +private: + friend class ColumnFormat; + friend class Printer; + + // Returns the column width as configured + // For each cell in the column, check the cell.format.width + // property and return the largest configured column width + // This is used to ensure that all cells in a column are + // aligned when printing the column + size_t get_configured_width() { + size_t result{0}; + for (size_t i = 0; i < size(); ++i) { + auto cell = cells_[i]; + auto format = cell.get().format(); + if (format.width_.has_value()) + result = std::max(result, *format.width_); + } + return result; + } + + // Computes the width of the column based on cell contents + // and configured cell padding + // For each cell, compute padding_left + cell_contents + padding_right + // and return the largest value + // + // This is useful when no cell.format.width is configured + // Call get_configured_width() + // - If this returns 0, then use get_computed_width() + size_t get_computed_width() { + size_t result{0}; + for (size_t i = 0; i < size(); ++i) { + result = std::max(result, get_cell_width(i)); + } + return result; + } + + // Returns padding_left + cell_contents.size() + padding_right + // for a given cell in the column + size_t get_cell_width(size_t cell_index) { + size_t result{0}; + Cell &cell = cells_[cell_index].get(); + auto format = cell.format(); + if (format.padding_left_.has_value()) + result += *format.padding_left_; + + // Check if input text has newlines + auto text = cell.get_text(); + auto split_lines = Format::split_lines(text, "\n", cell.locale(), + cell.is_multi_byte_character_support_enabled()); + + // If there are no newlines in input, set column_width = text.size() + if (split_lines.size() == 1) { + result += cell.size(); + } else { + // There are newlines in input + // Find widest substring in input and use this as column_width + size_t widest_sub_string_size{0}; + for (auto &line : split_lines) + if (get_sequence_length(line, cell.locale(), + cell.is_multi_byte_character_support_enabled()) > + widest_sub_string_size) + widest_sub_string_size = get_sequence_length( + line, cell.locale(), cell.is_multi_byte_character_support_enabled()); + result += widest_sub_string_size; + } + + if (format.padding_right_.has_value()) + result += *format.padding_right_; + + return result; + } + + std::vector> cells_; + std::weak_ptr parent_; +}; + +inline ColumnFormat &ColumnFormat::width(size_t value) { + for (auto &cell : column_.get().cells_) + cell.get().format().width(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::height(size_t value) { + for (auto &cell : column_.get().cells_) + cell.get().format().height(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::padding(size_t value) { + for (auto &cell : column_.get().cells_) + cell.get().format().padding(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::padding_left(size_t value) { + for (auto &cell : column_.get().cells_) + cell.get().format().padding_left(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::padding_right(size_t value) { + for (auto &cell : column_.get().cells_) + cell.get().format().padding_right(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::padding_top(size_t value) { + for (auto &cell : column_.get().cells_) + cell.get().format().padding_top(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::padding_bottom(size_t value) { + for (auto &cell : column_.get().cells_) + cell.get().format().padding_bottom(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_left(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_left(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_left_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_left_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_left_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_left_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_right(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_right(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_right_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_right_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_right_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_right_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_top(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_top(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_top_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_top_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_top_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_top_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_bottom(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_bottom(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_bottom_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_bottom_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::border_bottom_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().border_bottom_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::corner(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().corner(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::corner_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().corner_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::corner_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().corner_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::column_separator(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().column_separator(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::column_separator_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().column_separator_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::column_separator_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().column_separator_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::font_align(FontAlign value) { + for (auto &cell : column_.get().cells_) + cell.get().format().font_align(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::font_style(const std::vector &style) { + for (auto &cell : column_.get().cells_) + cell.get().format().font_style(style); + return *this; +} + +inline ColumnFormat &ColumnFormat::font_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().font_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::font_background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().font_background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::background_color(Color value) { + for (auto &cell : column_.get().cells_) + cell.get().format().background_color(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::multi_byte_characters(bool value) { + for (auto &cell : column_.get().cells_) + cell.get().format().multi_byte_characters(value); + return *this; +} + +inline ColumnFormat &ColumnFormat::locale(const std::string &value) { + for (auto &cell : column_.get().cells_) + cell.get().format().locale(value); + return *this; +} + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +// #include +#include +#include + +namespace tabulate { + +class Printer { +public: + static std::pair, std::vector> + compute_cell_dimensions(TableInternal &table); + + static void print_table(std::ostream &stream, TableInternal &table); + + static void print_row_in_cell(std::ostream &stream, TableInternal &table, + const std::pair &index, + const std::pair &dimension, size_t num_columns, + size_t row_index); + + static bool print_cell_border_top(std::ostream &stream, TableInternal &table, + const std::pair &index, + const std::pair &dimension, size_t num_columns); + static bool print_cell_border_bottom(std::ostream &stream, TableInternal &table, + const std::pair &index, + const std::pair &dimension, + size_t num_columns); + + static void apply_element_style(std::ostream &stream, Color foreground_color, + Color background_color, + const std::vector &font_style) { + apply_foreground_color(stream, foreground_color); + apply_background_color(stream, background_color); + for (auto &style : font_style) + apply_font_style(stream, style); + } + + static void reset_element_style(std::ostream &stream) { stream << termcolor::reset; } + +private: + static void print_content_left_aligned(std::ostream &stream, const std::string &cell_content, + const Format &format, size_t text_with_padding_size, + size_t column_width) { + + // Apply font style + apply_element_style(stream, *format.font_color_, *format.font_background_color_, + *format.font_style_); + stream << cell_content; + // Only apply font_style to the font + // Not the padding. So calling apply_element_style with font_style = {} + reset_element_style(stream); + apply_element_style(stream, *format.font_color_, *format.font_background_color_, {}); + + if (text_with_padding_size < column_width) { + for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) { + stream << " "; + } + } + } + + static void print_content_center_aligned(std::ostream &stream, const std::string &cell_content, + const Format &format, size_t text_with_padding_size, + size_t column_width) { + auto num_spaces = column_width - text_with_padding_size; + if (num_spaces % 2 == 0) { + // Even spacing on either side + for (size_t j = 0; j < num_spaces / 2; ++j) + stream << " "; + + // Apply font style + apply_element_style(stream, *format.font_color_, *format.font_background_color_, + *format.font_style_); + stream << cell_content; + // Only apply font_style to the font + // Not the padding. So calling apply_element_style with font_style = {} + reset_element_style(stream); + apply_element_style(stream, *format.font_color_, *format.font_background_color_, {}); + + for (size_t j = 0; j < num_spaces / 2; ++j) + stream << " "; + } else { + auto num_spaces_before = num_spaces / 2 + 1; + for (size_t j = 0; j < num_spaces_before; ++j) + stream << " "; + + // Apply font style + apply_element_style(stream, *format.font_color_, *format.font_background_color_, + *format.font_style_); + stream << cell_content; + // Only apply font_style to the font + // Not the padding. So calling apply_element_style with font_style = {} + reset_element_style(stream); + apply_element_style(stream, *format.font_color_, *format.font_background_color_, {}); + + for (size_t j = 0; j < num_spaces - num_spaces_before; ++j) + stream << " "; + } + } + + static void print_content_right_aligned(std::ostream &stream, const std::string &cell_content, + const Format &format, size_t text_with_padding_size, + size_t column_width) { + if (text_with_padding_size < column_width) { + for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) { + stream << " "; + } + } + + // Apply font style + apply_element_style(stream, *format.font_color_, *format.font_background_color_, + *format.font_style_); + stream << cell_content; + // Only apply font_style to the font + // Not the padding. So calling apply_element_style with font_style = {} + reset_element_style(stream); + apply_element_style(stream, *format.font_color_, *format.font_background_color_, {}); + } + + static void apply_font_style(std::ostream &stream, FontStyle style) { + switch (style) { + case FontStyle::bold: + stream << termcolor::bold; + break; + case FontStyle::dark: + stream << termcolor::dark; + break; + case FontStyle::italic: + stream << termcolor::italic; + break; + case FontStyle::underline: + stream << termcolor::underline; + break; + case FontStyle::blink: + stream << termcolor::blink; + break; + case FontStyle::reverse: + stream << termcolor::reverse; + break; + case FontStyle::concealed: + stream << termcolor::concealed; + break; + case FontStyle::crossed: + stream << termcolor::crossed; + break; + default: + break; + } + } + + static void apply_foreground_color(std::ostream &stream, Color foreground_color) { + switch (foreground_color) { + case Color::grey: + stream << termcolor::grey; + break; + case Color::red: + stream << termcolor::red; + break; + case Color::green: + stream << termcolor::green; + break; + case Color::yellow: + stream << termcolor::yellow; + break; + case Color::blue: + stream << termcolor::blue; + break; + case Color::magenta: + stream << termcolor::magenta; + break; + case Color::cyan: + stream << termcolor::cyan; + break; + case Color::white: + stream << termcolor::white; + break; + case Color::none: + default: + break; + } + } + + static void apply_background_color(std::ostream &stream, Color background_color) { + switch (background_color) { + case Color::grey: + stream << termcolor::on_grey; + break; + case Color::red: + stream << termcolor::on_red; + break; + case Color::green: + stream << termcolor::on_green; + break; + case Color::yellow: + stream << termcolor::on_yellow; + break; + case Color::blue: + stream << termcolor::on_blue; + break; + case Color::magenta: + stream << termcolor::on_magenta; + break; + case Color::cyan: + stream << termcolor::on_cyan; + break; + case Color::white: + stream << termcolor::on_white; + break; + case Color::none: + default: + break; + } + } +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +#include +#include +// #include +// #include +// #include +// #include +// #include +#include +#ifdef max +#undef max +#endif +#ifdef min +#undef min +#endif + +namespace tabulate { + +class TableInternal : public std::enable_shared_from_this { +public: + static std::shared_ptr create() { + auto result = std::shared_ptr(new TableInternal()); + result->format_.set_defaults(); + return result; + } + + void add_row(const std::vector &cells) { + auto row = std::make_shared(shared_from_this()); + for (auto &c : cells) { + auto cell = std::make_shared(row); + cell->set_text(c); + row->add_cell(cell); + } + rows_.push_back(row); + } + + Row &operator[](size_t index) { return *(rows_[index]); } + + const Row &operator[](size_t index) const { return *(rows_[index]); } + + Column column(size_t index) { + Column column(shared_from_this()); + for (size_t i = 0; i < rows_.size(); ++i) { + auto row = rows_[i]; + auto &cell = row->cell(index); + column.add_cell(cell); + } + return column; + } + + size_t size() const { return rows_.size(); } + + std::pair shape() { + std::pair result{0, 0}; + std::stringstream stream; + print(stream); + auto buffer = stream.str(); + auto lines = Format::split_lines(buffer, "\n", "", true); + if (lines.size()) { + result = {get_sequence_length(lines[0], "", true), lines.size()}; + } + return result; + } + + Format &format() { return format_; } + + void print(std::ostream &stream) { Printer::print_table(stream, *this); } + + size_t estimate_num_columns() const { + size_t result{0}; + if (size()) { + auto first_row = operator[](size_t(0)); + result = first_row.size(); + } + return result; + } + +private: + friend class Table; + friend class MarkdownExporter; + + TableInternal() {} + TableInternal &operator=(const TableInternal &); + TableInternal(const TableInternal &); + + std::vector> rows_; + Format format_; +}; + +inline Format &Cell::format() { + std::shared_ptr parent = parent_.lock(); + if (!format_.has_value()) { // no cell format + format_ = parent->format(); // Use parent row format + } else { + // Cell has formatting + // Merge cell formatting with parent row formatting + format_ = Format::merge(*format_, parent->format()); + } + return *format_; +} + +inline bool Cell::is_multi_byte_character_support_enabled() { + return (*format().multi_byte_characters_); +} + +inline Format &Row::format() { + std::shared_ptr parent = parent_.lock(); + if (!format_.has_value()) { // no row format + format_ = parent->format(); // Use parent table format + } else { + // Row has formatting rules + // Merge with parent table format + format_ = Format::merge(*format_, parent->format()); + } + return *format_; +} + +inline std::pair, std::vector> +Printer::compute_cell_dimensions(TableInternal &table) { + std::pair, std::vector> result; + size_t num_rows = table.size(); + size_t num_columns = table.estimate_num_columns(); + + std::vector row_heights, column_widths{}; + + for (size_t i = 0; i < num_columns; ++i) { + Column column = table.column(i); + size_t configured_width = column.get_configured_width(); + size_t computed_width = column.get_computed_width(); + if (configured_width != 0) + column_widths.push_back(configured_width); + else + column_widths.push_back(computed_width); + } + + for (size_t i = 0; i < num_rows; ++i) { + Row row = table[i]; + size_t configured_height = row.get_configured_height(); + size_t computed_height = row.get_computed_height(column_widths); + + // NOTE: Unlike column width, row height is calculated as the max + // b/w configured height and computed height + // which means that .width() has higher precedence than .height() + // when both are configured by the user + // + // TODO: Maybe this can be configured? + // If such a configuration is exposed, i.e., prefer height over width + // then the logic will be reversed, i.e., + // column_widths.push_back(std::max(configured_width, computed_width)) + // and + // row_height = configured_height if != 0 else computed_height + + row_heights.push_back(std::max(configured_height, computed_height)); + } + + result.first = row_heights; + result.second = column_widths; + + return result; +} + +inline void Printer::print_table(std::ostream &stream, TableInternal &table) { + size_t num_rows = table.size(); + size_t num_columns = table.estimate_num_columns(); + auto dimensions = compute_cell_dimensions(table); + auto row_heights = dimensions.first; + auto column_widths = dimensions.second; + + // For each row, + for (size_t i = 0; i < num_rows; ++i) { + + // Print top border + bool border_top_printed{true}; + for (size_t j = 0; j < num_columns; ++j) { + border_top_printed &= print_cell_border_top(stream, table, {i, j}, + {row_heights[i], column_widths[j]}, num_columns); + } + if (border_top_printed) + stream << termcolor::reset << "\n"; + + // Print row contents with word wrapping + for (size_t k = 0; k < row_heights[i]; ++k) { + for (size_t j = 0; j < num_columns; ++j) { + print_row_in_cell(stream, table, {i, j}, {row_heights[i], column_widths[j]}, num_columns, + k); + } + if (k + 1 < row_heights[i]) + stream << termcolor::reset << "\n"; + } + + if (i + 1 == num_rows) { + + // Check if there is bottom border to print: + auto bottom_border_needed{true}; + for (size_t j = 0; j < num_columns; ++j) { + auto cell = table[i][j]; + auto format = cell.format(); + auto corner = *format.corner_bottom_left_; + auto border_bottom = *format.border_bottom_; + if (corner == "" && border_bottom == "") { + bottom_border_needed = false; + break; + } + } + + if (bottom_border_needed) + stream << termcolor::reset << "\n"; + // Print bottom border for table + for (size_t j = 0; j < num_columns; ++j) { + print_cell_border_bottom(stream, table, {i, j}, {row_heights[i], column_widths[j]}, + num_columns); + } + } + if (i + 1 < num_rows) + stream << termcolor::reset << "\n"; // Don't add newline after last row + } +} + +inline void Printer::print_row_in_cell(std::ostream &stream, TableInternal &table, + const std::pair &index, + const std::pair &dimension, + size_t num_columns, size_t row_index) { + auto column_width = dimension.second; + auto cell = table[index.first][index.second]; + auto locale = cell.locale(); + auto is_multi_byte_character_support_enabled = cell.is_multi_byte_character_support_enabled(); + auto old_locale = std::locale::global(std::locale(locale)); + auto format = cell.format(); + auto text = cell.get_text(); + auto word_wrapped_text = + Format::word_wrap(text, column_width, locale, is_multi_byte_character_support_enabled); + auto text_height = std::count(word_wrapped_text.begin(), word_wrapped_text.end(), '\n') + 1; + auto padding_top = *format.padding_top_; + + if (*format.show_border_left_) { + apply_element_style(stream, *format.border_left_color_, *format.border_left_background_color_, + {}); + stream << *format.border_left_; + reset_element_style(stream); + } + + apply_element_style(stream, *format.font_color_, *format.font_background_color_, {}); + if (row_index < padding_top) { + // Padding top + stream << std::string(column_width, ' '); + } else if (row_index >= padding_top && (row_index <= (padding_top + text_height))) { + // // Row contents + + // Retrieve padding left and right + // (column_width - padding_left - padding_right) is the amount of space + // available for cell text - Use this to word wrap cell contents + auto padding_left = *format.padding_left_; + auto padding_right = *format.padding_right_; + + // Check if input text has embedded \n that are to be respected + auto newlines_in_input = Format::split_lines(text, "\n", cell.locale(), + cell.is_multi_byte_character_support_enabled()) + .size() - + 1; + std::string word_wrapped_text; + + // If there are no embedded \n characters, then apply word wrap + if (newlines_in_input == 0) { + // Apply word wrapping to input text + // Then display one word-wrapped line at a time within cell + if (column_width > (padding_left + padding_right)) + word_wrapped_text = + Format::word_wrap(text, column_width - padding_left - padding_right, cell.locale(), + cell.is_multi_byte_character_support_enabled()); + else { + // Configured column width cannot be lower than (padding_left + padding_right) + // This is a bad configuration + // E.g., the user is trying to force the column width to be 5 + // when padding_left and padding_right are each configured to 3 + // (padding_left + padding_right) = 6 > column_width + } + } else { + word_wrapped_text = text; // repect the embedded '\n' characters + } + + auto lines = Format::split_lines(word_wrapped_text, "\n", cell.locale(), + cell.is_multi_byte_character_support_enabled()); + + if (row_index - padding_top < lines.size()) { + auto line = lines[row_index - padding_top]; + + // Print left padding characters + stream << std::string(padding_left, ' '); + + // Print word-wrapped line + line = Format::trim(line); + auto line_with_padding_size = + get_sequence_length(line, cell.locale(), cell.is_multi_byte_character_support_enabled()) + + padding_left + padding_right; + switch (*format.font_align_) { + case FontAlign::left: + print_content_left_aligned(stream, line, format, line_with_padding_size, column_width); + break; + case FontAlign::center: + print_content_center_aligned(stream, line, format, line_with_padding_size, column_width); + break; + case FontAlign::right: + print_content_right_aligned(stream, line, format, line_with_padding_size, column_width); + break; + } + + // Print right padding characters + stream << std::string(padding_right, ' '); + } else + stream << std::string(column_width, ' '); + + } else { + // Padding bottom + stream << std::string(column_width, ' '); + } + + reset_element_style(stream); + + if (index.second + 1 == num_columns) { + // Print right border after last column + if (*format.show_border_right_) { + apply_element_style(stream, *format.border_right_color_, + *format.border_right_background_color_, {}); + stream << *format.border_right_; + reset_element_style(stream); + } + } + std::locale::global(old_locale); +} + +inline bool Printer::print_cell_border_top(std::ostream &stream, TableInternal &table, + const std::pair &index, + const std::pair &dimension, + size_t num_columns) { + auto cell = table[index.first][index.second]; + auto locale = cell.locale(); + auto old_locale = std::locale::global(std::locale(locale)); + auto format = cell.format(); + auto column_width = dimension.second; + + auto corner = *format.corner_top_left_; + auto corner_color = *format.corner_top_left_color_; + auto corner_background_color = *format.corner_top_left_background_color_; + auto border_top = *format.border_top_; + + if ((corner == "" && border_top == "") || !*format.show_border_top_) + return false; + + apply_element_style(stream, corner_color, corner_background_color, {}); + stream << corner; + reset_element_style(stream); + + for (size_t i = 0; i < column_width; ++i) { + apply_element_style(stream, *format.border_top_color_, *format.border_top_background_color_, + {}); + stream << border_top; + reset_element_style(stream); + } + + if (index.second + 1 == num_columns) { + // Print corner after last column + corner = *format.corner_top_right_; + corner_color = *format.corner_top_right_color_; + corner_background_color = *format.corner_top_right_background_color_; + + apply_element_style(stream, corner_color, corner_background_color, {}); + stream << corner; + reset_element_style(stream); + } + std::locale::global(old_locale); + return true; +} + +inline bool Printer::print_cell_border_bottom(std::ostream &stream, TableInternal &table, + const std::pair &index, + const std::pair &dimension, + size_t num_columns) { + auto cell = table[index.first][index.second]; + auto locale = cell.locale(); + auto old_locale = std::locale::global(std::locale(locale)); + auto format = cell.format(); + auto column_width = dimension.second; + + auto corner = *format.corner_bottom_left_; + auto corner_color = *format.corner_bottom_left_color_; + auto corner_background_color = *format.corner_bottom_left_background_color_; + auto border_bottom = *format.border_bottom_; + + if ((corner == "" && border_bottom == "") || !*format.show_border_bottom_) + return false; + + apply_element_style(stream, corner_color, corner_background_color, {}); + stream << corner; + reset_element_style(stream); + + for (size_t i = 0; i < column_width; ++i) { + apply_element_style(stream, *format.border_bottom_color_, + *format.border_bottom_background_color_, {}); + stream << border_bottom; + reset_element_style(stream); + } + + if (index.second + 1 == num_columns) { + // Print corner after last column + corner = *format.corner_bottom_right_; + corner_color = *format.corner_bottom_right_color_; + corner_background_color = *format.corner_bottom_right_background_color_; + + apply_element_style(stream, corner_color, corner_background_color, {}); + stream << corner; + reset_element_style(stream); + } + std::locale::global(old_locale); + return true; +} + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 + +#if __cplusplus >= 201703L +#include +#include +using std::get_if; +using std::holds_alternative; +using std::variant; +using std::visit; +using std::string_view; +#else +// #include +// #include +using nonstd::get_if; +using nonstd::holds_alternative; +using nonstd::variant; +using nonstd::visit; +using nonstd::string_view; +#endif + +#include + +namespace tabulate { + +class Table { +public: + Table() : table_(TableInternal::create()) {} + + using Row_t = std::vector>; + + Table &add_row(const Row_t &cells) { + + if (rows_ == 0) { + // This is the first row added + // cells.size() is the number of columns + cols_ = cells.size(); + } + + std::vector cell_strings; + if (cells.size() < cols_) { + cell_strings.resize(cols_); + std::fill(cell_strings.begin(), cell_strings.end(), ""); + } else { + cell_strings.resize(cells.size()); + std::fill(cell_strings.begin(), cell_strings.end(), ""); + } + + for (size_t i = 0; i < cells.size(); ++i) { + auto cell = cells[i]; + if (holds_alternative(cell)) { + cell_strings[i] = *get_if(&cell); + } else if (holds_alternative(cell)) { + cell_strings[i] = *get_if(&cell); + } else if (holds_alternative(cell)) { + cell_strings[i] = std::string{*get_if(&cell)}; + } else { + auto table = *get_if(&cell); + std::stringstream stream; + table.print(stream); + cell_strings[i] = stream.str(); + } + } + + table_->add_row(cell_strings); + rows_ += 1; + return *this; + } + + Row &operator[](size_t index) { return row(index); } + + Row &row(size_t index) { return (*table_)[index]; } + + Column column(size_t index) { return table_->column(index); } + + Format &format() { return table_->format(); } + + void print(std::ostream &stream) { table_->print(stream); } + + std::string str() { + std::stringstream stream; + print(stream); + return stream.str(); + } + + std::pair shape() { return table_->shape(); } + + class RowIterator { + public: + explicit RowIterator(std::vector>::iterator ptr) : ptr(ptr) {} + + RowIterator operator++() { + ++ptr; + return *this; + } + bool operator!=(const RowIterator &other) const { return ptr != other.ptr; } + Row &operator*() { return **ptr; } + + private: + std::vector>::iterator ptr; + }; + + auto begin() -> RowIterator { return RowIterator(table_->rows_.begin()); } + auto end() -> RowIterator { return RowIterator(table_->rows_.end()); } + +private: + friend class MarkdownExporter; + friend class LatexExporter; + friend class AsciiDocExporter; + + friend std::ostream &operator<<(std::ostream &stream, const Table &table); + size_t rows_{0}; + size_t cols_{0}; + std::shared_ptr table_; +}; + +inline std::ostream &operator<<(std::ostream &stream, const Table &table) { + const_cast
(table).print(stream); + return stream; +} + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +// #include + +namespace tabulate { + +class Exporter { +public: + virtual std::string dump(Table &table) = 0; + virtual ~Exporter() {} +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 + +namespace tabulate { + +class MarkdownExporter : public Exporter { +public: + std::string dump(Table &table) override { + std::string result{""}; + apply_markdown_format(table); + result = table.str(); + restore_table_format(table); + return result; + } + + virtual ~MarkdownExporter() {} + +private: + void add_alignment_header_row(Table &table) { + auto &rows = table.table_->rows_; + + if (rows.size() >= 1) { + auto alignment_row = std::make_shared(table.table_->shared_from_this()); + + // Create alignment header cells + std::vector alignment_cells{}; + for (auto &cell : table[0]) { + auto format = cell.format(); + if (format.font_align_.value() == FontAlign::left) { + alignment_cells.push_back(":----"); + } else if (format.font_align_.value() == FontAlign::center) { + alignment_cells.push_back(":---:"); + } else if (format.font_align_.value() == FontAlign::right) { + alignment_cells.push_back("----:"); + } + } + + // Add alignment header cells to alignment row + for (auto &c : alignment_cells) { + auto cell = std::make_shared(alignment_row); + cell->format() + .hide_border_top() + .hide_border_bottom() + .border_left("|") + .border_right("|") + .column_separator("|") + .corner("|"); + cell->set_text(c); + if (c == ":---:") + cell->format().font_align(FontAlign::center); + else if (c == "----:") + cell->format().font_align(FontAlign::right); + alignment_row->add_cell(cell); + } + + // Insert alignment header row + if (rows.size() > 1) + rows.insert(rows.begin() + 1, alignment_row); + else + rows.push_back(alignment_row); + } + } + + void remove_alignment_header_row(Table &table) { + auto &rows = table.table_->rows_; + table.table_->rows_.erase(rows.begin() + 1); + } + + void apply_markdown_format(Table &table) { + // Apply markdown format to cells in each row + for (auto row : table) { + for (auto &cell : row) { + auto format = cell.format(); + formats_.push_back(format); + cell.format() + .hide_border_top() + .hide_border_bottom() + .border_left("|") + .border_right("|") + .column_separator("|") + .corner("|"); + } + } + // Add alignment header row at position 1 + add_alignment_header_row(table); + } + + void restore_table_format(Table &table) { + // Remove alignment header row at position 1 + remove_alignment_header_row(table); + + // Restore original formatting for each cell + size_t format_index{0}; + for (auto row : table) { + for (auto &cell : row) { + cell.format() = formats_[format_index]; + format_index += 1; + } + } + } + + std::vector formats_; +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 + +#if __cplusplus >= 201703L +#include +using std::optional; +#else +// #include +using nonstd::optional; +#endif + +namespace tabulate { + +class LatexExporter : public Exporter { + + static const char new_line = '\n'; + +public: + class ExportOptions { + public: + ExportOptions &indentation(std::size_t value) { + indentation_ = value; + return *this; + } + + private: + friend class LatexExporter; + optional indentation_; + }; + + ExportOptions &configure() { return options_; } + + std::string dump(Table &table) override { + std::string result{"\\begin{tabular}"}; + result += new_line; + + result += add_alignment_header(table); + result += new_line; + const auto rows = table.rows_; + // iterate content and put text into the table. + for (size_t i = 0; i < rows; i++) { + auto &row = table[i]; + // apply row content indentation + if (options_.indentation_.has_value()) { + result += std::string(options_.indentation_.value(), ' '); + } + + for (size_t j = 0; j < row.size(); j++) { + + result += row[j].get_text(); + + // check column position, need "\\" at the end of each row + if (j < row.size() - 1) { + result += " & "; + } else { + result += " \\\\"; + } + } + result += new_line; + } + + result += "\\end{tabular}"; + return result; + } + + virtual ~LatexExporter() {} + +private: + std::string add_alignment_header(Table &table) { + std::string result{"{"}; + + for (auto &cell : table[0]) { + auto format = cell.format(); + if (format.font_align_.value() == FontAlign::left) { + result += 'l'; + } else if (format.font_align_.value() == FontAlign::center) { + result += 'c'; + } else if (format.font_align_.value() == FontAlign::right) { + result += 'r'; + } + } + + result += "}"; + return result; + } + ExportOptions options_; +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 +#include +#include +#include +// #include + +namespace tabulate { + +class AsciiDocExporter : public Exporter { + + static const char new_line = '\n'; + +public: + std::string dump(Table &table) override { + std::stringstream ss; + ss << add_alignment_header(table); + ss << new_line; + + const auto rows = table.rows_; + // iterate content and put text into the table. + for (size_t row_index = 0; row_index < rows; row_index++) { + auto &row = table[row_index]; + + for (size_t cell_index = 0; cell_index < row.size(); cell_index++) { + ss << "|"; + ss << add_formatted_cell(row[cell_index]); + } + ss << new_line; + if (row_index == 0) { + ss << new_line; + } + } + + ss << "|==="; + return ss.str(); + } + + virtual ~AsciiDocExporter() {} + +private: + std::string add_formatted_cell(Cell &cell) const { + std::stringstream ss; + auto format = cell.format(); + std::string cell_string = cell.get_text(); + + auto font_style = format.font_style_.value(); + + bool format_bold = false; + bool format_italic = false; + std::for_each(font_style.begin(), font_style.end(), [&](FontStyle &style) { + if (style == FontStyle::bold) { + format_bold = true; + } else if (style == FontStyle::italic) { + format_italic = true; + } + }); + + if (format_bold) { + ss << '*'; + } + if (format_italic) { + ss << '_'; + } + + ss << cell_string; + if (format_italic) { + ss << '_'; + } + if (format_bold) { + ss << '*'; + } + return ss.str(); + } + + std::string add_alignment_header(Table &table) { + std::stringstream ss; + ss << (R"([cols=")"); + + size_t column_count = table[0].size(); + size_t column_index = 0; + for (auto &cell : table[0]) { + auto format = cell.format(); + + if (format.font_align_.value() == FontAlign::left) { + ss << '<'; + } else if (format.font_align_.value() == FontAlign::center) { + ss << '^'; + } else if (format.font_align_.value() == FontAlign::right) { + ss << '>'; + } + + ++column_index; + if (column_index != column_count) { + ss << ","; + } + } + + ss << R"("])"; + ss << new_line; + ss << "|==="; + + return ss.str(); + } +}; + +} // namespace tabulate + +/* + __ ___. .__ __ +_/ |______ \_ |__ __ __| | _____ _/ |_ ____ +\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \ + | | / __ \| \_\ \ | / |__/ __ \| | \ ___/ + |__| (____ /___ /____/|____(____ /__| \___ > + \/ \/ \/ \/ +Table Maker for Modern C++ +https://github.com/p-ranav/tabulate + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +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 TABULATE_EXPORT_HPP +#define TABULATE_EXPORT_HPP + +// #ifdef _WIN32 +// #ifdef TABULATE_STATIC_LIB +// #define TABULATE_API +// #else +// #ifdef TABULATE_EXPORTS +// #define TABULATE_API __declspec(dllexport) +// #else +// #define TABULATE_API __declspec(dllimport) +// #endif +// #endif +// #else +// #define TABULATE_API +// #endif + +// Project version +#define TABULATE_VERSION_MAJOR 1 +#define TABULATE_VERSION_MINOR 4 +#define TABULATE_VERSION_PATCH 0 + +// Composing the protocol version string from major, and minor +#define TABULATE_CONCATENATE(A, B) TABULATE_CONCATENATE_IMPL(A, B) +#define TABULATE_CONCATENATE_IMPL(A, B) A##B +#define TABULATE_STRINGIFY(a) TABULATE_STRINGIFY_IMPL(a) +#define TABULATE_STRINGIFY_IMPL(a) #a + +#endif diff --git a/lib/tokenize.cc b/lib/tokenize.cc new file mode 100644 index 0000000..96a8708 --- /dev/null +++ b/lib/tokenize.cc @@ -0,0 +1,38 @@ +/* +** Copyright (C) 2017-2020 Dirk-Jan C. Binnema +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include "mu-tokenizer.hh" + +int +main(int argc, char* argv[]) +{ + std::string s; + + for (auto i = 1; i < argc; ++i) + s += " " + std::string(argv[i]); + + const auto tvec = Mu::tokenize(s); + for (const auto& t : tvec) + std::cout << t << std::endl; + + return 0; +} diff --git a/lib/utils/Makefile.am b/lib/utils/Makefile.am new file mode 100644 index 0000000..422b1de --- /dev/null +++ b/lib/utils/Makefile.am @@ -0,0 +1,74 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CFLAGS= \ + $(WARN_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(ASAN_CFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \ + -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \ + -Wno-format-nonliteral \ + -Wno-switch-enum \ + -Wno-deprecated-declarations \ + -Wno-inline \ + -I${top_srcdir}/lib + +AM_CPPFLAGS= \ + $(CODE_COVERAGE_CPPFLAGS) + +AM_CXXFLAGS= \ + $(WARN_CXXFLAGS) \ + $(GLIB_CFLAGS) \ + $(ASAN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -I${top_srcdir}/lib + +AM_LDFLAGS= \ + $(ASAN_LDFLAGS) + +noinst_LTLIBRARIES= \ + libmu-utils.la + +libmu_utils_la_SOURCES= \ + mu-async-queue.hh \ + mu-command-parser.cc \ + mu-command-parser.hh \ + mu-error.hh \ + mu-logger.cc \ + mu-logger.hh \ + mu-option.hh \ + mu-option.cc \ + mu-readline.cc \ + mu-readline.hh \ + mu-result.hh \ + mu-sexp.cc \ + mu-sexp.hh \ + mu-util.c \ + mu-util.h \ + mu-utils.cc \ + mu-utils.hh \ + mu-utils-format.hh \ + mu-xapian-utils.hh + +libmu_utils_la_LIBADD= \ + $(GLIB_LIBS) \ + $(READLINE_LIBS) \ + $(CODE_COVERAGE_LIBS) + +include $(top_srcdir)/aminclude_static.am diff --git a/lib/utils/meson.build b/lib/utils/meson.build new file mode 100644 index 0000000..6277396 --- /dev/null +++ b/lib/utils/meson.build @@ -0,0 +1,42 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +lib_mu_utils=static_library('mu-utils', [ + 'mu-command-parser.cc', + 'mu-logger.cc', + 'mu-option.cc', + 'mu-readline.cc', + 'mu-sexp.cc', + 'mu-test-utils.cc', + 'mu-util.c', + 'mu-util.h', + 'mu-utils.cc'], + dependencies: [ + glib_dep, + gio_dep, + config_h_dep, + readline_dep + ], + include_directories: include_directories(['.','..']), + install: false) + +lib_mu_utils_dep = declare_dependency( + link_with: lib_mu_utils, + include_directories: include_directories(['.', '..']) +) + +subdir('tests') diff --git a/lib/utils/mu-async-queue.hh b/lib/utils/mu-async-queue.hh new file mode 100644 index 0000000..bc3e655 --- /dev/null +++ b/lib/utils/mu-async-queue.hh @@ -0,0 +1,199 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_ASYNC_QUEUE_HH__ +#define __MU_ASYNC_QUEUE_HH__ + +#include +#include +#include +#include + +namespace Mu { + +constexpr std::size_t UnlimitedAsyncQueueSize{0}; + +template > /**< allocator the items */ + +class AsyncQueue { + public: + using value_type = ItemType; + using allocator_type = Allocator; + using size_type = std::size_t; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = typename std::allocator_traits::pointer; + using const_pointer = typename std::allocator_traits::const_pointer; + + using Timeout = std::chrono::steady_clock::duration; + +#define LOCKED std::unique_lock lock(m_); + + /** + * Push an item to the end of the queue by moving it + * + * @param item the item to move to the end of the queue + * @param timeout and optional timeout + * + * @return true if the item was pushed; false otherwise. + */ + bool push(const value_type& item, Timeout timeout = {}) + { + return push(std::move(value_type(item)), timeout); + } + + /** + * Push an item to the end of the queue by moving it + * + * @param item the item to move to the end of the queue + * @param timeout and optional timeout + * + * @return true if the item was pushed; false otherwise. + */ + bool push(value_type&& item, Timeout timeout = {}) + { + std::unique_lock lock{m_}; + + if (!unlimited()) { + const auto rv = cv_full_.wait_for(lock, timeout, [&]() { + return !full_unlocked(); + }) && !full_unlocked(); + if (!rv) + return false; + } + + q_.emplace_back(std::move(item)); + cv_empty_.notify_one(); + + return true; + } + + /** + * Pop an item from the queue + * + * @param receives the value if the function returns true + * @param timeout optional time to wait for an item to become available + * + * @return true if an item was popped (into val), false otherwise. + */ + bool pop(value_type& val, Timeout timeout = {}) + { + std::unique_lock lock{m_}; + + if (timeout != Timeout{}) { + const auto rv = cv_empty_.wait_for(lock, timeout, [&]() { + return !q_.empty(); + }) && !q_.empty(); + if (!rv) + return false; + + } else if (q_.empty()) + return false; + + val = std::move(q_.front()); + q_.pop_front(); + cv_full_.notify_one(); + + return true; + } + + /** + * Clear the queue + * + */ + void clear() + { + std::unique_lock lock{m_}; + q_.clear(); + cv_full_.notify_one(); + } + + /** + * Size of the queue + * + * + * @return the size + */ + size_type size() const + { + std::unique_lock lock{m_}; + return q_.size(); + } + + /** + * Maximum size of the queue if specified through the template + * parameter; otherwise the (theoretical) max_size of the inner + * container. + * + * @return the maximum size + */ + size_type max_size() const + { + if (unlimited()) + return q_.max_size(); + else + return MaxSize; + } + + /** + * Is the queue empty? + * + * @return true or false + */ + bool empty() const + { + std::unique_lock lock{m_}; + return q_.empty(); + } + + /** + * Is the queue full? Returns false unless a maximum size was specified + * (as a template argument) + * + * @return true or false. + */ + bool full() const + { + if (unlimited()) + return false; + + std::unique_lock lock{m_}; + return full_unlocked(); + } + + /** + * Is this queue (theoretically) unlimited in size? + * + * @return true or false + */ + constexpr static bool unlimited() { return MaxSize == UnlimitedAsyncQueueSize; } + +private: + bool full_unlocked() const { return q_.size() >= max_size(); } + + std::deque q_; + mutable std::mutex m_; + std::condition_variable cv_full_, cv_empty_; +}; + +} // namespace Mu + +#endif /* __MU_ASYNC_QUEUE_HH__ */ diff --git a/lib/utils/mu-command-parser.cc b/lib/utils/mu-command-parser.cc new file mode 100644 index 0000000..40be1e9 --- /dev/null +++ b/lib/utils/mu-command-parser.cc @@ -0,0 +1,204 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-command-parser.hh" +#include "mu-error.hh" +#include "mu-utils.hh" + +#include +#include + +using namespace Mu; +using namespace Command; + +void +Command::invoke(const Command::CommandMap& cmap, const Sexp& call) +{ + if (!call.is_call()) { + throw Mu::Error{Error::Code::Command, + "expected call-sexpr but got %s", + call.to_sexp_string().c_str()}; + } + + const auto& params{call.list()}; + const auto cmd_it = cmap.find(params.at(0).value()); + if (cmd_it == cmap.end()) + throw Mu::Error{Error::Code::Command, + "unknown command in call %s", + call.to_sexp_string().c_str()}; + + const auto& cinfo{cmd_it->second}; + + // all required parameters must be present + for (auto&& arg : cinfo.args) { + const auto& argname{arg.first}; + const auto& arginfo{arg.second}; + + // calls used keyword-parameters, e.g. + // (my-function :bar 1 :cuux "fnorb") + // so, we're looking for the odd-numbered parameters. + const auto param_it = [&]() -> Sexp::Seq::const_iterator { + for (size_t i = 1; i < params.size(); i += 2) + if (params.at(i).is_symbol() && params.at(i).value() == argname) + return params.begin() + i + 1; + + return params.end(); + }(); + + // it's an error when a required parameter is missing. + if (param_it == params.end()) { + if (arginfo.required) + throw Mu::Error{Error::Code::Command, + "missing required parameter %s in call %s", + argname.c_str(), + call.to_sexp_string().c_str()}; + continue; // not required + } + + // the types must match, but the 'nil' symbol is acceptable as + // "no value" + if (param_it->type() != arginfo.type && !(param_it->is_nil())) + throw Mu::Error{Error::Code::Command, + "parameter %s expects type %s, but got %s in call %s", + argname.c_str(), + to_string(arginfo.type).c_str(), + to_string(param_it->type()).c_str(), + call.to_sexp_string().c_str()}; + } + + // all passed parameters must be known + for (size_t i = 1; i < params.size(); i += 2) { + if (std::none_of(cinfo.args.begin(), cinfo.args.end(), [&](auto&& arg) { + return params.at(i).value() == arg.first; + })) + throw Mu::Error{Error::Code::Command, + "unknown parameter %s in call %s", + params.at(i).value().c_str(), + call.to_sexp_string().c_str()}; + } + + if (cinfo.handler) + cinfo.handler(params); +} + +static Sexp::Seq::const_iterator +find_param_node(const Parameters& params, const std::string& argname) +{ + if (params.empty()) + throw Error(Error::Code::InvalidArgument, "params must not be empty"); + + if (argname.empty() || argname.at(0) != ':') + throw Error(Error::Code::InvalidArgument, + "property key must start with ':' but got '%s')", + argname.c_str()); + + for (size_t i = 1; i < params.size(); i += 2) { + if (i + 1 != params.size() && params.at(i).is_symbol() && + params.at(i).value() == argname) + return params.begin() + i + 1; + } + + return params.end(); +} + +static Error +wrong_type(Sexp::Type expected, Sexp::Type got) +{ + return Error(Error::Code::InvalidArgument, + "expected <%s> but got <%s>", + to_string(expected).c_str(), + to_string(got).c_str()); +} + +Option +Command::get_string(const Parameters& params, const std::string& argname) +{ + const auto it = find_param_node(params, argname); + if (it == params.end() || it->is_nil()) + return Nothing; + else if (!it->is_string()) + throw wrong_type(Sexp::Type::String, it->type()); + else + return it->value(); +} + +Option +Command::get_symbol(const Parameters& params, const std::string& argname) +{ + const auto it = find_param_node(params, argname); + if (it == params.end() || it->is_nil()) + return Nothing; + else if (!it->is_symbol()) + throw wrong_type(Sexp::Type::Symbol, it->type()); + else + return it->value(); +} + +Option +Command::get_int(const Parameters& params, const std::string& argname) +{ + const auto it = find_param_node(params, argname); + if (it == params.end() || it->is_nil()) + return Nothing; + else if (!it->is_number()) + throw wrong_type(Sexp::Type::Number, it->type()); + else + return ::atoi(it->value().c_str()); +} + +Option +Command::get_unsigned(const Parameters& params, const std::string& argname) +{ + if (auto val = get_int(params, argname); val && *val >= 0) + return val; + else + return Nothing; +} + + +Option +Command::get_bool(const Parameters& params, const std::string& argname) +{ + const auto it = find_param_node(params, argname); + if (it == params.end()) + return Nothing; + else if (!it->is_symbol()) + throw wrong_type(Sexp::Type::Symbol, it->type()); + else + return it->is_nil() ? false : true; +} + +std::vector +Command::get_string_vec(const Parameters& params, const std::string& argname) +{ + const auto it = find_param_node(params, argname); + if (it == params.end() || it->is_nil()) + return {}; + else if (!it->is_list()) + throw wrong_type(Sexp::Type::List, it->type()); + + std::vector vec; + for (const auto& n : it->list()) { + if (!n.is_string()) + throw wrong_type(Sexp::Type::String, n.type()); + vec.emplace_back(n.value()); + } + + return vec; +} diff --git a/lib/utils/mu-command-parser.hh b/lib/utils/mu-command-parser.hh new file mode 100644 index 0000000..751e5d8 --- /dev/null +++ b/lib/utils/mu-command-parser.hh @@ -0,0 +1,180 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#ifndef MU_COMMAND_PARSER_HH__ +#define MU_COMMAND_PARSER_HH__ + +#include +#include +#include +#include +#include +#include +#include + +#include "utils/mu-error.hh" +#include "utils/mu-sexp.hh" +#include "utils/mu-option.hh" + +namespace Mu { +namespace Command { + +/// +/// Commands are s-expressions with the follow properties: + +/// 1) a command is a list with a command-name as its first argument +/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some +/// type (ie. 'keyword arguments') +/// 3) each command is described by its CommandInfo structure, which defines the type +/// 4) calls to the command must include all required parameters +/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed +/// for specify a non-required parameter to be absent; this is for convenience on the +/// call side. + +/// Information about a function argument +struct ArgInfo { + ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg) + : type{typearg}, required{requiredarg}, docstring{std::move(docarg)} + { + } + const Sexp::Type type; /**< Sexp::Type of the argument */ + const bool required; /**< Is this argument required? */ + const std::string docstring; /**< Documentation */ +}; + +/// The arguments for a function, which maps their names to the information. +using ArgMap = std::unordered_map; +// The parameters to a Handler. +using Parameters = Sexp::Seq; + +Option get_int(const Parameters& parms, const std::string& argname); +Option get_unsigned(const Parameters& parms, const std::string& argname); +Option get_bool(const Parameters& parms, const std::string& argname); +Option get_string(const Parameters& parms, const std::string& argname); +Option get_symbol(const Parameters& parms, const std::string& argname); + +std::vector get_string_vec(const Parameters& params, const std::string& argname); + +/* + * backward compat + */ +static inline int +get_int_or(const Parameters& parms, const std::string& arg, int alt = 0) { + return get_int(parms, arg).value_or(alt); +} + +static inline bool +get_bool_or(const Parameters& parms, const std::string& arg, bool alt = false) { + return get_bool(parms, arg).value_or(alt); +} +static inline std::string +get_string_or(const Parameters& parms, const std::string& arg, const std::string& alt = ""){ + return get_string(parms, arg).value_or(alt); +} + +static inline std::string +get_symbol_or(const Parameters& parms, const std::string& arg, const std::string& alt = "nil") { + return get_symbol(parms, arg).value_or(alt); +} + + + + +// A handler function +using Handler = std::function; + +/// Information about some command +struct CommandInfo { + CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg) + : args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{ + std::move(handlerarg)} + { + } + const ArgMap args; + const std::string docstring; + const Handler handler; + + /** + * Get a sorted list of argument names, for display. Required args come + * first, then alphabetical. + * + * @return vec with the sorted names. + */ + std::vector sorted_argnames() const + { // sort args -- by required, then alphabetical. + std::vector names; + for (auto&& arg : args) + names.emplace_back(arg.first); + std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) { + const auto& arg1{args.find(name1)->second}; + const auto& arg2{args.find(name2)->second}; + if (arg1.required != arg2.required) + return arg1.required; + else + return name1 < name2; + }); + return names; + } +}; +/// All commands, mapping their name to information about them. +using CommandMap = std::unordered_map; + +/** + * Validate that the call (a Sexp) specifies a valid call, then invoke it. + * + * A call uses keyword arguments, e.g. something like: + * (foo :bar 1 :cuux "fnorb") + * + * On error, throw Error. + * + * @param cmap map of commands + * @param call node describing a call. + */ +void invoke(const Command::CommandMap& cmap, const Sexp& call); + +static inline std::ostream& +operator<<(std::ostream& os, const Command::ArgInfo& info) +{ + os << info.type << " (" << (info.required ? "required" : "optional") << ")"; + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Command::CommandInfo& info) +{ + for (auto&& arg : info.args) + os << " " << arg.first << " " << arg.second << '\n' + << " " << arg.second.docstring << "\n"; + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Command::CommandMap& map) +{ + for (auto&& c : map) + os << c.first << '\n' << c.second; + + return os; +} + +} // namespace Command +} // namespace Mu + +#endif /* MU_COMMAND_PARSER_HH__ */ diff --git a/lib/utils/mu-error.hh b/lib/utils/mu-error.hh new file mode 100644 index 0000000..c67fc5a --- /dev/null +++ b/lib/utils/mu-error.hh @@ -0,0 +1,174 @@ +/* +** Copyright (C) 2019-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_ERROR_HH__ +#define MU_ERROR_HH__ + +#include +#include "mu-utils-format.hh" +#include "mu-util.h" +#include + +namespace Mu { + +struct Error final : public std::exception { + + // 16 lower bits are for the error code the next 8 bits is for the return code + // upper byte is for flags + + static constexpr uint32_t SoftError = 1 << 23; + +#define ERROR_ENUM(RV,CAT) \ + static_cast(__LINE__ | ((RV) << 15) | (CAT)) + + enum struct Code: uint32_t { + AccessDenied = ERROR_ENUM(1,0), + AssertionFailure = ERROR_ENUM(1,0), + Command = ERROR_ENUM(1,0), + ContactNotFound = ERROR_ENUM(2,SoftError), + Crypto = ERROR_ENUM(1,0), + File = ERROR_ENUM(1,0), + Index = ERROR_ENUM(1,0), + Internal = ERROR_ENUM(1,0), + InvalidArgument = ERROR_ENUM(1,0), + Message = ERROR_ENUM(1,0), + NoMatches = ERROR_ENUM(4,SoftError), + NotFound = ERROR_ENUM(1,0), + Parsing = ERROR_ENUM(1,0), + Play = ERROR_ENUM(1,0), + Query = ERROR_ENUM(1,0), + SchemaMismatch = ERROR_ENUM(1,0), + Store = ERROR_ENUM(1,0), + StoreLock = ERROR_ENUM(19,0), + UnverifiedSignature = ERROR_ENUM(1,0), + User = ERROR_ENUM(1,0), + Xapian = ERROR_ENUM(1,0), + }; + + + /** + * Construct an error + * + * @param codearg error-code + * #param msgarg the error diecription + */ + Error(Code codearg, const std::string& msgarg) : code_{codearg}, what_{msgarg} {} + Error(Code codearg, std::string&& msgarg) : code_{codearg}, what_{std::move(msgarg)} {} + + /** + * Build an error from an error-code and a format string + * + * @param code error-code + * @param frm format string + * @param ... format parameters + * + * @return an Error object + */ + __attribute__((format(printf, 3, 0))) Error(Code codearg, const char* frm, ...) + : code_{codearg} + { + va_list args; + va_start(args, frm); + what_ = vformat(frm, args); + va_end(args); + } + + Error(Error&& rhs) = default; + Error(const Error& rhs) = default; + + /** + * Build an error from a GError an error-code and a format string + * + * @param code error-code + * @param gerr a GError or {}, which is consumed + * @param frm format string + * @param ... format parameters + * + * @return an Error object + */ + __attribute__((format(printf, 4, 0))) + Error(Code codearg, GError** err, const char* frm, ...) + : code_{codearg} + { + va_list args; + va_start(args, frm); + what_ = vformat(frm, args); + va_end(args); + + if (err && *err) + what_ += format(": %s", (*err)->message); + else + what_ += ": something went wrong"; + + g_clear_error(err); + } + + /** + * DTOR + * + */ + virtual ~Error() override = default; + + /** + * Get the descriptive message. + * + * @return + */ + virtual const char* what() const noexcept override { return what_.c_str(); } + + /** + * Get the error-code for this error + * + * @return the error-code + */ + Code code() const noexcept { return code_; } + + + /** + * Is this is a 'soft error'? + * + * @return true or false + */ + constexpr bool is_soft_error() const { + return (static_cast(code_) & SoftError) != 0; + } + + constexpr uint8_t exit_code() const { + return ((static_cast(code_) >> 15) & 0xff); + } + + + /** + * Fill a GError with the error information + * + * @param err GError** (or NULL) + */ + void fill_g_error(GError **err) const noexcept{ + g_set_error(err, MU_ERROR_DOMAIN, static_cast(code_), + "%s", what_.c_str()); + } + +private: + const Code code_; + std::string what_; +}; + +} // namespace Mu + +#endif /* MU_ERROR_HH__ */ diff --git a/lib/utils/mu-logger.cc b/lib/utils/mu-logger.cc new file mode 100644 index 0000000..40ac4e0 --- /dev/null +++ b/lib/utils/mu-logger.cc @@ -0,0 +1,182 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#define G_LOG_USE_STRUCTURED +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "mu-logger.hh" + +using namespace Mu; + +static bool MuLogInitialized = false; +static Mu::LogOptions MuLogOptions; +static std::ofstream MuStream; +static auto MaxLogFileSize = 1000 * 1024; + +static std::string MuLogPath; + +static bool +maybe_open_logfile() +{ + if (MuStream.is_open()) + return true; + + MuStream.open(MuLogPath, std::ios::out | std::ios::app); + if (!MuStream.is_open()) { + std::cerr << "opening " << MuLogPath << " failed:" << g_strerror(errno) + << std::endl; + return false; + } + + MuStream.sync_with_stdio(false); + return true; +} + +static bool +maybe_rotate_logfile() +{ + static unsigned n = 0; + + if (n++ % 1000 != 0) + return true; + + GStatBuf statbuf; + if (g_stat(MuLogPath.c_str(), &statbuf) == -1 || statbuf.st_size <= MaxLogFileSize) + return true; + + const auto old = MuLogPath + ".old"; + g_unlink(old.c_str()); // opportunistic + + if (MuStream.is_open()) + MuStream.close(); + + if (g_rename(MuLogPath.c_str(), old.c_str()) != 0) + std::cerr << "failed to rename " << MuLogPath << " -> " << old.c_str() << ": " + << g_strerror(errno) << std::endl; + + return maybe_open_logfile(); +} + +static GLogWriterOutput +log_file(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data) +{ + if (!maybe_open_logfile()) + return G_LOG_WRITER_UNHANDLED; + + char timebuf[22]; + time_t now{::time(NULL)}; + ::strftime(timebuf, sizeof(timebuf), "%F %T", ::localtime(&now)); + + char* msg = g_log_writer_format_fields(level, fields, n_fields, FALSE); + if (msg && msg[0] == '\n') // hmm... seems lines start with '\n'r + msg[0] = ' '; + + MuStream << timebuf << ' ' << msg << std::endl; + + g_free(msg); + + return maybe_rotate_logfile() ? G_LOG_WRITER_HANDLED : G_LOG_WRITER_UNHANDLED; +} + +static GLogWriterOutput +log_stdouterr(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data) +{ + return g_log_writer_standard_streams(level, fields, n_fields, user_data); +} + +static GLogWriterOutput +log_journal(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data) +{ + return g_log_writer_journald(level, fields, n_fields, user_data); +} + +void +Mu::log_init(const std::string& path, Mu::LogOptions opts) +{ + if (MuLogInitialized) { + g_error("logging is already initialized"); + return; + } + + if (g_getenv("MU_LOG_STDOUTERR")) + opts |= LogOptions::StdOutErr; + + MuLogOptions = opts; + MuLogPath = path; + + g_log_set_writer_func( + [](GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data) { + // filter out debug-level messages? + if (level == G_LOG_LEVEL_DEBUG && + (none_of(MuLogOptions & Mu::LogOptions::Debug))) + return G_LOG_WRITER_HANDLED; + + // log criticals to stdout / err or if asked + if (level == G_LOG_LEVEL_CRITICAL || + any_of(MuLogOptions & Mu::LogOptions::StdOutErr)) { + log_stdouterr(level, fields, n_fields, user_data); + } + + // log to the journal, or, if not available to a file. + if (log_journal(level, fields, n_fields, user_data) != G_LOG_WRITER_HANDLED) + return log_file(level, fields, n_fields, user_data); + else + return G_LOG_WRITER_HANDLED; + }, + NULL, + NULL); + + g_message("logging initialized; debug: %s, stdout/stderr: %s", + any_of(log_get_options() & LogOptions::Debug) ? "yes" : "no", + any_of(log_get_options() & LogOptions::StdOutErr) ? "yes" : "no"); + + MuLogInitialized = true; +} + +void +Mu::log_uninit() +{ + if (!MuLogInitialized) + return; + + if (MuStream.is_open()) + MuStream.close(); + + MuLogInitialized = false; +} + +void +Mu::log_set_options(Mu::LogOptions opts) +{ + MuLogOptions = opts; +} + +Mu::LogOptions +Mu::log_get_options() +{ + return MuLogOptions; +} diff --git a/lib/utils/mu-logger.hh b/lib/utils/mu-logger.hh new file mode 100644 index 0000000..61b187b --- /dev/null +++ b/lib/utils/mu-logger.hh @@ -0,0 +1,74 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_LOGGER_HH__ +#define MU_LOGGER_HH__ + +#include +#include "utils/mu-utils.hh" + +namespace Mu { + +/** + * Logging options + * + */ +enum struct LogOptions { + None = 0, /**< Nothing specific */ + StdOutErr = 1 << 1, /**< Log to stdout/stderr */ + Debug = 1 << 2, /**< Include debug-level logs */ +}; + +/** + * Initialize the logging system. Note that the path is only used if structured + * logging fails -- practically, it goes to the file if there's + * systemd/journald. + * + * if the environment variable MU_LOG_STDOUTERR is set, LogOptions::StdoutErr is + * implied. + * + * @param path path to the log file + * @param opts logging options + */ +void log_init(const std::string& path, LogOptions opts); + +/** + * Uninitialize the logging system + * + */ +void log_uninit(); + +/** + * Change the logging options. + * + * @param opts options + */ +void log_set_options(LogOptions opts); + +/** + * Get the current log options + * + * @return the log options + */ +LogOptions log_get_options(); + +} // namespace Mu +MU_ENABLE_BITOPS(Mu::LogOptions); + +#endif /* MU_LOGGER_HH__ */ diff --git a/lib/utils/mu-option.cc b/lib/utils/mu-option.cc new file mode 100644 index 0000000..a136b86 --- /dev/null +++ b/lib/utils/mu-option.cc @@ -0,0 +1,32 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-option.hh" +#include + +using namespace Mu; + +Mu::Option +Mu::to_string_opt_gchar(gchar*&& str) +{ + auto res = to_string_opt(str); + g_free(str); + + return res; +} diff --git a/lib/utils/mu-option.hh b/lib/utils/mu-option.hh new file mode 100644 index 0000000..6af72e1 --- /dev/null +++ b/lib/utils/mu-option.hh @@ -0,0 +1,60 @@ +/* + * Created on 2020-11-08 by Dirk-Jan C. Binnema + * + * Copyright (c) 2020 Logitech, Inc. All Rights Reserved + * This program is a trade secret of LOGITECH, and it is not to be reproduced, + * published, disclosed to others, copied, adapted, distributed or displayed + * without the prior authorization of LOGITECH. + * + * Licensee agrees to attach or embed this notice on all copies of the program, + * including partial copies or modified versions thereof. + * + */ + +#ifndef MU_OPTION__ +#define MU_OPTION__ + +#include "thirdparty/optional.hpp" +#include + +namespace Mu { + +/// Either a value of type T, or None +template using Option = tl::optional; + +template +Option +Some(T&& t) +{ + return std::move(t); +} +constexpr auto Nothing = tl::nullopt; // 'None' is take already + +/** + * Maybe create a string from a const char pointer. + * + * @param str a char pointer or NULL + * + * @return option with either the string or nothing if str was NULL. + */ +Option +static inline to_string_opt(const char* str) { + if (str) + return std::string{str}; + else + return Nothing; +} + +/** + * Like maybe_string that takes a const char*, but additionally, + * g_free() the string. + * + * @param str char pointer or NULL (consumed) + * + * @return option with either the string or nothing if str was NULL. + */ +Option to_string_opt_gchar(char*&& str); + + +} // namespace Mu +#endif /*MU_OPTION__*/ diff --git a/lib/utils/mu-readline.cc b/lib/utils/mu-readline.cc new file mode 100644 index 0000000..2f9c72a --- /dev/null +++ b/lib/utils/mu-readline.cc @@ -0,0 +1,135 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-readline.hh" +#include "config.h" + +#include +#include +#include +#include +#include + +#ifdef HAVE_LIBREADLINE +#if defined(HAVE_READLINE_READLINE_H) +#include +#elif defined(HAVE_READLINE_H) +#include +#else /* !defined(HAVE_READLINE_H) */ +extern char* readline(); +#endif /* !defined(HAVE_READLINE_H) */ +char* cmdline = NULL; +#else /* !defined(HAVE_READLINE_READLINE_H) */ +/* no readline */ +#endif /* HAVE_LIBREADLINE */ + +#ifdef HAVE_READLINE_HISTORY +#if defined(HAVE_READLINE_HISTORY_H) +#include +#elif defined(HAVE_HISTORY_H) +#include +#else /* !defined(HAVE_HISTORY_H) */ +extern void add_history(); +extern int write_history(); +extern int read_history(); +#endif /* defined(HAVE_READLINE_HISTORY_H) */ +/* no history */ +#endif /* HAVE_READLINE_HISTORY */ + +#if defined(HAVE_LIBREADLINE) && defined(HAVE_READLINE_HISTORY) +#define HAVE_READLINE (1) +#else +#define HAVE_READLINE (0) +#endif + +using namespace Mu; + +static bool is_a_tty{}; +static std::string hist_path; +static size_t max_lines{}; + +bool +Mu::have_readline() +{ + return HAVE_READLINE != 0; +} + + +void +Mu::setup_readline(const std::string& histpath, size_t maxlines) +{ + is_a_tty = !!::isatty(::fileno(stdout)); + hist_path = histpath; + max_lines = maxlines; + +#if HAVE_READLINE + rl_bind_key('\t', rl_insert); // default (filenames) is not useful + using_history(); + read_history(hist_path.c_str()); + + if (max_lines > 0) + stifle_history(max_lines); +#endif /*HAVE_READLINE*/ +} + +void +Mu::shutdown_readline() +{ +#if HAVE_READLINE + if (!is_a_tty) + return; + + write_history(hist_path.c_str()); + if (max_lines > 0) + history_truncate_file(hist_path.c_str(), max_lines); +#endif /*HAVE_READLINE*/ +} + +std::string +Mu::read_line(bool& do_quit) +{ +#if HAVE_READLINE + if (is_a_tty) { + auto buf = readline(";; mu% "); + if (!buf) { + do_quit = true; + return {}; + } + std::string line{buf}; + ::free(buf); + return line; + } +#endif /*HAVE_READLINE*/ + + std::string line; + std::cout << ";; mu> "; + if (!std::getline(std::cin, line)) + do_quit = true; + + return line; +} + +void +Mu::save_line(const std::string& line) +{ +#if HAVE_READLINE + if (is_a_tty) + add_history(line.c_str()); +#endif /*HAVE_READLINE*/ +} diff --git a/lib/utils/mu-readline.hh b/lib/utils/mu-readline.hh new file mode 100644 index 0000000..ca0455f --- /dev/null +++ b/lib/utils/mu-readline.hh @@ -0,0 +1,61 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include + +namespace Mu { + +/** + * Setup readline when available and on tty. + * + * @param histpath path to the history file + * @param max_lines maximum number of history to save + */ +void setup_readline(const std::string& histpath, size_t max_lines); + +/** + * Shutdown readline + * + */ +void shutdown_readline(); + +/** + * Read a command line + * + * @param do_quit recceives whether we should quit. + * + * @return the string read or empty + */ +std::string read_line(bool& do_quit); + +/** + * Save a line to history (or do nothing when readline is not active) + * + * @param line a line. + */ +void save_line(const std::string& line); + + +/** + * Do we have the non-shim readline? + * + * @return true or failse + */ +bool have_readline(); + +} // namespace Mu diff --git a/lib/utils/mu-result.hh b/lib/utils/mu-result.hh new file mode 100644 index 0000000..ee709f1 --- /dev/null +++ b/lib/utils/mu-result.hh @@ -0,0 +1,157 @@ +/* +** Copyright (C) 2019-2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_RESULT_HH__ +#define MU_RESULT_HH__ + +#include "thirdparty/expected.hpp" +#include "utils/mu-error.hh" + +namespace Mu { +/** + * A little Rust-envy...a Result is _either_ some value of type T, _or_ a Mu::Error + */ +template using Result = tl::expected; + +/** + * Ok() is not typically strictly needed (unlike Err), but imitates Rust's Ok + * and it helps the reader. + * + * @param t the value to return + * + * @return a success Result + */ +template +class Result::expected +// note: "class", not "typename"; +// https://stackoverflow.com/questions/46412754/class-name-injection-and-constructors +Ok(T&& t) +{ + return std::move(t); +} + +/** + * Implementation of Ok() for void results. + * + * @return a success Result + */ +static inline Result +Ok() +{ + return {}; +} + +/** + * Return an error + * + * @param err the error + * + * @return error + */ +static inline tl::unexpected +Err(Error&& err) +{ + return tl::unexpected(std::move(err)); +} + +static inline tl::unexpected +Err(const Error& err) +{ + return tl::unexpected(err); +} + +template +static inline tl::unexpected +Err(const Result& res) +{ + return res.error(); +} + + + +template +static inline Result +Ok(const T& t) +{ + if (t) + return Ok(); + else + return Err(t.error()); +} + + +/* + * convenience + */ + +static inline tl::unexpected +Err(Error::Code errcode, std::string&& msg="") +{ + return Err(Error{errcode, std::move(msg)}); +} + +__attribute__((format(printf, 2, 0))) +static inline tl::unexpected +Err(Error::Code errcode, const char* frm, ...) +{ + va_list args; + va_start(args, frm); + auto str{vformat(frm, args)}; + va_end(args); + + return Err(errcode, std::move(str)); +} + +__attribute__((format(printf, 3, 0))) +static inline tl::unexpected +Err(Error::Code errcode, GError **err, const char* frm, ...) +{ + va_list args; + va_start(args, frm); + auto str{vformat(frm, args)}; + va_end(args); + + if (err && *err) + str += format(" (%s)", (*err)->message ? (*err)->message : ""); + g_clear_error(err); + + return Err(errcode, std::move(str)); +} + + + +/** + * Assert that some result has a value (for unit tests) + * + * @param R some result + */ +#define assert_valid_result(R) do { \ + if(!R) { \ + g_printerr("%s:%u: error-result: %s\n", \ + __FILE__, __LINE__, \ + (R).error().what()); \ + g_assert_true(!!R); \ + } \ +} while(0) + + + +}// namespace Mu + +#endif /* MU_RESULT_HH__ */ diff --git a/lib/utils/mu-sexp.cc b/lib/utils/mu-sexp.cc new file mode 100644 index 0000000..a8e1ff6 --- /dev/null +++ b/lib/utils/mu-sexp.cc @@ -0,0 +1,270 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-sexp.hh" +#include "mu-utils.hh" + +#include +#include + +using namespace Mu; + +__attribute__((format(printf, 2, 0))) static Mu::Error +parsing_error(size_t pos, const char* frm, ...) +{ + va_list args; + va_start(args, frm); + auto msg = vformat(frm, args); + va_end(args); + + if (pos == 0) + return Mu::Error(Error::Code::Parsing, "%s", msg.c_str()); + else + return Mu::Error(Error::Code::Parsing, "%zu: %s", pos, msg.c_str()); +} +static size_t +skip_whitespace(const std::string& s, size_t pos) +{ + while (pos != s.size()) { + if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n') + ++pos; + else + break; + } + + return pos; +} + +static Sexp parse(const std::string& expr, size_t& pos); + +static Sexp +parse_list(const std::string& expr, size_t& pos) +{ + if (expr[pos] != '(') // sanity check. + throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]); + + Sexp::List list; + + ++pos; + while (expr[pos] != ')' && pos != expr.size()) + list.add(parse(expr, pos)); + + if (expr[pos] != ')') + throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]); + ++pos; + return Sexp::make_list(std::move(list)); +} + +// parse string +static Sexp +parse_string(const std::string& expr, size_t& pos) +{ + if (expr[pos] != '"') // sanity check. + throw parsing_error(pos, "expected: '\"'' but got '%c", expr[pos]); + + bool escape{}; + std::string str; + for (++pos; pos != expr.size(); ++pos) { + auto kar = expr[pos]; + if (escape && (kar == '"' || kar == '\\')) { + str += kar; + escape = false; + continue; + } + + if (kar == '"') + break; + else if (kar == '\\') + escape = true; + else + str += kar; + } + + if (escape || expr[pos] != '"') + throw parsing_error(pos, "unterminated string '%s'", str.c_str()); + + ++pos; + return Sexp::make_string(std::move(str)); +} + +static Sexp +parse_integer(const std::string& expr, size_t& pos) +{ + if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check. + throw parsing_error(pos, "expected: but got '%c", expr[pos]); + + std::string num; // negative number? + if (expr[pos] == '-') { + num = "-"; + ++pos; + } + + for (; isdigit(expr[pos]); ++pos) + num += expr[pos]; + + return Sexp::make_number(::atoi(num.c_str())); +} + +static Sexp +parse_symbol(const std::string& expr, size_t& pos) +{ + if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check. + throw parsing_error(pos, "expected: |: but got '%c", expr[pos]); + + std::string symbol(1, expr[pos]); + for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos) + symbol += expr[pos]; + + return Sexp::make_symbol(std::move(symbol)); +} + +static Sexp +parse(const std::string& expr, size_t& pos) +{ + pos = skip_whitespace(expr, pos); + + if (pos == expr.size()) + throw parsing_error(pos, "expected: character '%c", expr[pos]); + + const auto kar = expr[pos]; + const auto node = [&]() -> Sexp { + if (kar == '(') + return parse_list(expr, pos); + else if (kar == '"') + return parse_string(expr, pos); + else if (isdigit(kar) || kar == '-') + return parse_integer(expr, pos); + else if (isalpha(kar) || kar == ':') + return parse_symbol(expr, pos); + else + throw parsing_error(pos, "unexpected character '%c", kar); + }(); + + pos = skip_whitespace(expr, pos); + + return node; +} + +Sexp +Sexp::make_parse(const std::string& expr) +{ + size_t pos{}; + auto node{::parse(expr, pos)}; + + if (pos != expr.size()) + throw parsing_error(pos, "trailing data starting with '%c'", expr[pos]); + + return node; +} + +std::string +Sexp::to_sexp_string() const +{ + std::stringstream sstrm; + + switch (type()) { + case Type::List: { + sstrm << '('; + bool first{true}; + for (auto&& child : list()) { + sstrm << (first ? "" : " ") << child.to_sexp_string(); + first = false; + } + sstrm << ')'; + + if (any_of(formatting_opts & FormattingOptions::SplitList)) + sstrm << '\n'; + break; + } + case Type::String: + sstrm << quote(value()); + break; + case Type::Raw: + sstrm << value(); + break; + case Type::Number: + case Type::Symbol: + case Type::Empty: + default: sstrm << value(); + } + + return sstrm.str(); +} + +// LCOV_EXCL_START + +std::string +Sexp::to_json_string() const +{ + std::stringstream sstrm; + + switch (type()) { + case Type::List: { + // property-lists become JSON objects + if (is_prop_list()) { + sstrm << "{"; + auto it{list().begin()}; + bool first{true}; + while (it != list().end()) { + sstrm << (first ? "" : ",") << quote(it->value()) << ":"; + ++it; + sstrm << it->to_json_string(); + ++it; + first = false; + } + sstrm << "}"; + if (any_of(formatting_opts & FormattingOptions::SplitList)) + sstrm << '\n'; + } else { // other lists become arrays. + sstrm << '['; + bool first{true}; + for (auto&& child : list()) { + sstrm << (first ? "" : ", ") << child.to_json_string(); + first = false; + } + sstrm << ']'; + if (any_of(formatting_opts & FormattingOptions::SplitList)) + sstrm << '\n'; + } + break; + } + case Type::String: + sstrm << quote(value()); + break; + case Type::Raw: // FIXME: implement this. + break; + + case Type::Symbol: + if (is_nil()) + sstrm << "false"; + else if (is_t()) + sstrm << "true"; + else + sstrm << quote(value()); + break; + case Type::Number: + case Type::Empty: + default: sstrm << value(); + } + + return sstrm.str(); +} + +// LCOV_EXCL_STOP diff --git a/lib/utils/mu-sexp.hh b/lib/utils/mu-sexp.hh new file mode 100644 index 0000000..f04b735 --- /dev/null +++ b/lib/utils/mu-sexp.hh @@ -0,0 +1,447 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_SEXP_HH__ +#define MU_SEXP_HH__ + +#include +#include +#include + +#include "utils/mu-utils.hh" +#include "utils/mu-error.hh" + +namespace Mu { +/// Simple s-expression parser & list that parses lists () and atoms (strings +/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or +/// ':', then alphanum and '-') +/// +/// (:foo (1234 "bar" nil) :quux (a b c)) + +/// Parse node +struct Sexp { + /// Node type + enum struct Type { Empty, List, String, Number, Symbol, Raw }; + + /** + * Default CTOR + */ + Sexp() : type_{Type::Empty} {} + + // Underlying data type for list; we'd like to use std::dequeu here, + // but that does not compile with libc++ (it does with libstdc++) + using Seq = std::vector; + + /** + * Make a sexp out of an s-expression string. + * + * @param expr a string containing an s-expression + * + * @return the parsed s-expression, or throw Error. + */ + static Sexp make_parse(const std::string& expr); + + /** + * Make a node for a string/integer/symbol/list value + * + * @param val some value + * @param empty_is_nil turn empty string into a 'nil' symbol + * + * @return a node + */ + static Sexp make_string(std::string&& val, bool empty_is_nil=false) + { + if (empty_is_nil && val.empty()) + return make_symbol("nil"); + else + return Sexp{Type::String, std::move(val)}; + } + static Sexp make_string(const std::string& val, bool empty_is_nil=false) + { + if (empty_is_nil && val.empty()) + return make_symbol("nil"); + else + return Sexp{Type::String, std::string(val)}; + } + + static Sexp make_number(int val) { return Sexp{Type::Number, format("%d", val)}; } + static Sexp make_symbol(std::string&& val) { + if (val.empty()) + throw Error(Error::Code::InvalidArgument, + "symbol must be non-empty"); + return Sexp{Type::Symbol, std::move(val)}; + } + static Sexp make_symbol_sv(std::string_view val) { + return make_symbol(std::string{val}); + } + + /** + * Add a raw string sexp. + * + * @param val value + * + * @return A sexp + */ + static Sexp make_raw(std::string&& val) { + return Sexp{Type::Raw, std::string{val}}; + } + static Sexp make_raw(const std::string& val) { + return make_raw(std::string{val}); + } + + + /** + * + * + * The value of this node; invalid for list nodes. + * + * @return + */ + const std::string& value() const { + if (is_list()) + throw Error(Error::Code::InvalidArgument, "no value for list"); + if (is_empty()) + throw Error{Error::Code::InvalidArgument, "no value for empty"}; + return value_; + } + + /** + * The underlying container of this list node; only valid for lists + * + * @return + */ + const Seq& list() const { + if (!is_list()) + throw Error(Error::Code::InvalidArgument, "not a list"); + return seq_; + } + + /** + * Convert a Sexp to its S-expression string representation + * + * @return the string representation + */ + std::string to_sexp_string() const; + + /** + * Convert a Sexp::Node to its JSON string representation + * + * @return the string representation + */ + std::string to_json_string() const; + + /** + * Return the type of this Node. + * + * @return the type + */ + Type type() const { return type_; } + + /// + /// Helper struct to build mutable lists. + /// + struct List { + List () = default; + List (const Seq& seq): seq_{seq} {} + + /** + * Add a sexp to the list + * + * @param sexp a sexp + * @param args rest arguments + * + * @return a ref to this List (for chaining) + */ + List& add() { return *this; } + List& add(Sexp&& sexp) + { + seq_.emplace_back(std::move(sexp)); + return *this; + } + template List& add(Sexp&& sexp, Args... args) + { + seq_.emplace_back(std::move(sexp)); + seq_.emplace_back(std::forward(args)...); + return *this; + } + + /** + * Add a property (i.e., :key sexp ) to the list. Remove any + * prop with the same name + * + * @param name a property-name. Must start with ':', length > 1 + * @param sexp a sexp + * @param args rest arguments + * + * @return a ref to this List (for chaining) + */ + List& add_prop(std::string&& name, Sexp&& sexp) { + remove_prop(name); + if (!is_prop_name(name)) + throw Error{Error::Code::InvalidArgument, + "invalid property name ('%s')", + name.c_str()}; + seq_.emplace_back(make_symbol(std::move(name))); + seq_.emplace_back(std::move(sexp)); + return *this; + } + template + List& add_prop(std::string&& name, Sexp&& sexp, Args... args) { + remove_prop(name); + add_prop(std::move(name), std::move(sexp)); + add_prop(std::forward(args)...); + return *this; + } + + void remove_prop(const std::string& name) { + if (!is_prop_name(name)) + throw Error{Error::Code::InvalidArgument, + "invalid property name ('%s')", name.c_str()}; + auto it = std::find_if(seq_.begin(), seq_.end(), [&](auto&& elm) { + return elm.type() == Sexp::Type::Symbol && + elm.value() == name; + }); + if (it != seq_.cend() && it + 1 != seq_.cend()) { + /* erase propname and value.*/ + seq_.erase(it, it + 2); + } + } + + /** + * Remove all elements from the list. + */ + void clear() { seq_.clear(); } + + /** + * Get the number of elements in the list + * + * @return number + */ + size_t size() const { return seq_.size(); } + + /** + * Is the list empty? + * + * @return true or false + */ + size_t empty() const { return seq_.empty(); } + + private: + friend struct Sexp; + Seq seq_; + }; + + /** + * Construct a list sexp from a List + * + * @param list a list-list + * @param sexp a Sexp + * @param args rest arguments + * + * @return a sexp. + */ + static Sexp make_list(List&& list) { return Sexp{Type::List, std::move(list.seq_)}; } + template static Sexp make_list(Sexp&& sexp, Args... args) + { + List lst; + lst.add(std::move(sexp)).add(std::forward(args)...); + return make_list(std::move(lst)); + } + + /** + * Construct a property list sexp from a List + * + * @param name the property name; must start wtth ':' + * @param sexp a Sexp + * @param args rest arguments (property list) + * + * @return a sexp. + */ + template + static Sexp make_prop_list(std::string&& name, Sexp&& sexp, Args... args) + { + List list; + list.add_prop(std::move(name), std::move(sexp), std::forward(args)...); + return make_list(std::move(list)); + } + + /** + * Construct a properrty list sexp from a List + * + * @param funcname function name for the call + * @param name the property name; must start wtth ':' + * @param sexp a Sexp + * @param args rest arguments (property list) + * + * @return a sexp. + */ + template + static Sexp make_call(std::string&& funcname, std::string&& name, Sexp&& sexp, Args... args) + { + List list; + list.add(make_symbol(std::move(funcname))); + list.add_prop(std::move(name), std::move(sexp), std::forward(args)...); + return make_list(std::move(list)); + } + + /// Some type helpers + bool is_list() const { return type() == Type::List; } + bool is_string() const { return type() == Type::String; } + bool is_number() const { return type() == Type::Number; } + bool is_symbol() const { return type() == Type::Symbol; } + bool is_empty() const { return type() == Type::Empty; } + + operator bool() const { return !is_empty(); } + + static constexpr auto SymbolNil{"nil"}; + static constexpr auto SymbolT{"t"}; + bool is_nil() const { return is_symbol() && value() == SymbolNil; } + bool is_t() const { return is_symbol() && value() == SymbolT; } + + /** + * Is this a prop-list? A prop list is a list sexp with alternating + * property / sexp + * + * @return + */ + bool is_prop_list() const + { + if (!is_list() || list().size() % 2 != 0) + return false; + else + return is_prop_list(list().begin(), list().end()); + } + + /** + * Is this a call? A call is a list sexp with a symbol (function name), + * followed by a prop list + * + * @return + */ + bool is_call() const + { + if (!is_list() || list().size() % 2 != 1 || !list().at(0).is_symbol()) + return false; + else + return is_prop_list(list().begin() + 1, list().end()); + } + + enum struct FormattingOptions { + Default = 0, /**< Nothing in particular */ + SplitList = 1 << 0, /**< Insert newline after list item */ + }; + + FormattingOptions formatting_opts{}; /**< Formatting option for the + * string output */ + +private: + Sexp(Type typearg, std::string&& valuearg) : type_{typearg}, value_{std::move(valuearg)} { + if (is_list()) + throw Error{Error::Code::InvalidArgument, "cannot be a list type"}; + if (is_empty()) + throw Error{Error::Code::InvalidArgument, "cannot be an empty type"}; + } + Sexp(Type typearg, Seq&& seq) : type_{Type::List}, seq_{std::move(seq)} { + if (!is_list()) + throw Error{Error::Code::InvalidArgument, "must be a list type"}; + if (is_empty()) + throw Error{Error::Code::InvalidArgument, "cannot be an empty type"}; + } + /** + * Is the sexp a valid property name? + * + * @param sexp a Sexp. + * + * @return true or false. + */ + static bool is_prop_name(const std::string& str) + { + return str.size() > 1 && str.at(0) == ':'; + } + static bool is_prop_name(const Sexp& sexp) + { + return sexp.is_symbol() && is_prop_name(sexp.value()); + } + + static bool is_prop_list(Seq::const_iterator b, Seq::const_iterator e) + { + while (b != e) { + const Sexp& s{*b}; + if (!is_prop_name(s)) + return false; + if (++b == e) + return false; + ++b; + } + return b == e; + } + + Type type_; /**< Type of node */ + std::string value_; /**< String value of node (only for + * non-Type::Lst)*/ + Seq seq_; /**< Children of node (only for + * Type::Lst) */ +}; + +static inline std::ostream& +operator<<(std::ostream& os, Sexp::Type id) +{ + switch (id) { + case Sexp::Type::List: + os << "list"; + break; + case Sexp::Type::String: + os << "string"; + break; + case Sexp::Type::Number: + os << "number"; + break; + case Sexp::Type::Symbol: + os << "symbol"; + break; + case Sexp::Type::Raw: + os << "raw"; + break; + case Sexp::Type::Empty: + os << "empty"; + break; + default: throw std::runtime_error("unknown node type"); + } + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Sexp& sexp) +{ + os << sexp.to_sexp_string(); + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Sexp::List& sexp) +{ + os << Sexp::make_list(Sexp::List(sexp)); + return os; +} +MU_ENABLE_BITOPS(Sexp::FormattingOptions); + +} // namespace Mu + +#endif /* MU_SEXP_HH__ */ diff --git a/lib/utils/mu-test-utils.cc b/lib/utils/mu-test-utils.cc new file mode 100644 index 0000000..8e049b2 --- /dev/null +++ b/lib/utils/mu-test-utils.cc @@ -0,0 +1,150 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include +#include + +#include +#include +#include + +#include +#include + +#include "utils/mu-test-utils.hh" +#include "utils/mu-error.hh" + + +using namespace Mu; + +char* +Mu::test_mu_common_get_random_tmpdir() +{ + char* dir; + int res; + + dir = g_strdup_printf("%s%cmu-test-%d%ctest-%x", + g_get_tmp_dir(), + G_DIR_SEPARATOR, + getuid(), + G_DIR_SEPARATOR, + (int)random() * getpid() * (int)time(NULL)); + + res = g_mkdir_with_parents(dir, 0700); + g_assert(res != -1); + + return dir; +} + +const char* +Mu::set_tz(const char* tz) +{ + static const char* oldtz; + + oldtz = getenv("TZ"); + if (tz) + setenv("TZ", tz, 1); + else + unsetenv("TZ"); + + tzset(); + return oldtz; +} + +bool +Mu::set_en_us_utf8_locale() +{ + setenv("LC_ALL", "en_US.UTF-8", 1); + setlocale(LC_ALL, "en_US.UTF-8"); + + if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) { + g_print("Note: Unit tests require the en_US.utf8 locale. " + "Ignoring test cases.\n"); + return FALSE; + } + + return TRUE; +} + +static void +black_hole(void) +{ + return; /* do nothing */ +} + +void +Mu::mu_test_init(int *argc, char ***argv) +{ + g_test_init(argc, argv, NULL); + + if (!g_test_verbose()) + g_log_set_handler( + NULL, + (GLogLevelFlags)(G_LOG_LEVEL_MASK | + G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), + (GLogFunc)black_hole, NULL); +} + + + +void +Mu::allow_warnings() +{ + g_test_log_set_fatal_handler( + [](const char*, GLogLevelFlags, const char*, gpointer) { return FALSE; }, + {}); +} + + + +Mu::TempDir::TempDir(bool autodelete): autodelete_{autodelete} +{ + GError *err{}; + gchar *tmpdir = g_dir_make_tmp("mu-tmp-XXXXXX", &err); + if (!tmpdir) + throw Mu::Error(Error::Code::File, &err, + "failed to create temporary directory"); + + path_ = tmpdir; + g_free(tmpdir); + + g_debug("created '%s'", path_.c_str()); +} + +Mu::TempDir::~TempDir() +{ + if (::access(path_.c_str(), F_OK) != 0) + return; /* nothing to do */ + + if (!autodelete_) { + g_debug("_not_ deleting %s", path_.c_str()); + return; + } + + /* ugly */ + GError *err{}; + const auto cmd{format("/bin/rm -rf '%s'", path_.c_str())}; + if (!g_spawn_command_line_sync(cmd.c_str(), NULL, NULL, NULL, &err)) { + g_warning("error: %s\n", err ? err->message : "?"); + g_clear_error(&err); + } else + g_debug("removed '%s'", path_.c_str()); +} diff --git a/lib/utils/mu-test-utils.hh b/lib/utils/mu-test-utils.hh new file mode 100644 index 0000000..c59b9d5 --- /dev/null +++ b/lib/utils/mu-test-utils.hh @@ -0,0 +1,129 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_TEST_UTILS_HH__ +#define MU_TEST_UTILS_HH__ + +#include + +namespace Mu { + +/** + * get a dir name for a random temporary directory to do tests + * + * @return a random dir name, g_free when it's no longer needed + */ +char* test_mu_common_get_random_tmpdir(); + +/** + * mu wrapper for g_test_init + * + * @param argc + * @param argv + */ +void mu_test_init(int *argc, char ***argv); + +/** + * set the timezone + * + * @param tz timezone + * + * @return the old timezone + */ +const char* set_tz(const char* tz); + +/** + * switch the locale to en_US.utf8, return TRUE if it succeeds + * + * @return true if the switch succeeds, false otherwise + */ +bool set_en_us_utf8_locale(); + +/** + * For unit tests, assert two std::string's are equal. + * + * @param s1 string1 + * @param s2 string2 + */ +#define assert_equal(s1__,s2__) do { \ + std::string s1s__(s1__), s2s__(s2__); \ + g_assert_cmpstr(s1s__.c_str(), ==, s2s__.c_str()); \ + } while(0) + + +#define assert_equal_seq(seq1__, seq2__) do { \ + g_assert_cmpuint(seq1__.size(), ==, seq2__.size()); \ + size_t n__{}; \ + for (auto&& item__: seq1__) { \ + g_assert_true(item__ == seq2__.at(n__)); \ + ++n__; \ + } \ + } while(0) + +#define assert_equal_seq_str(seq1__, seq2__) do { \ + g_assert_cmpuint(seq1__.size(), ==, seq2__.size()); \ + size_t n__{}; \ + for (auto&& item__: seq1__) { \ + assert_equal(item__, seq2__.at(n__)); \ + ++n__; \ + } \ + } while(0) + +/** + * For unit-tests, allow warnings in the current function. + * + */ +void allow_warnings(); + + +/** + * For unit-tests, a RAII tempdir. + * + */ +struct TempDir { + /** + * Construct a temporary directory + */ + TempDir(bool autodelete=true); + + /** + * DTOR; removes the temporary directory + * + * + * @return + */ + ~TempDir(); + + /** + * Path to the temporary directory + * + * @return the path. + * + * + */ + const std::string& path() {return path_; } +private: + std::string path_; + const bool autodelete_; +}; + +} // namepace Mu + + +#endif /* MU_TEST_UTILS_HH__ */ diff --git a/lib/utils/mu-util.c b/lib/utils/mu-util.c new file mode 100644 index 0000000..a2e3bc6 --- /dev/null +++ b/lib/utils/mu-util.c @@ -0,0 +1,468 @@ +/* +** Copyright (C) 2008-2021 Dirk-Jan C. Binnema +** +** 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif /*_GNU_SOURCE*/ + +#include "mu-util.h" +#ifdef HAVE_WORDEXP_H +#include /* for shell-style globbing */ +#endif /*HAVE_WORDEXP_H*/ + +#include + +#include +#include /* for setlocale() */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + + +static char* +do_wordexp (const char *path) +{ +#ifdef HAVE_WORDEXP_H + wordexp_t wexp; + char *dir; + + if (!path) { + /* g_debug ("%s: path is empty", __func__); */ + return NULL; + } + + if (wordexp (path, &wexp, 0) != 0) { + /* g_debug ("%s: expansion failed for %s", __func__, path); */ + return NULL; + } + + /* we just pick the first one */ + dir = g_strdup (wexp.we_wordv[0]); + + /* strangely, below seems to lead to a crash on MacOS (BSD); + so we have to allow for a tiny leak here on that + platform... maybe instead of __APPLE__ it should be + __BSD__? + + Hmmm., cannot reproduce that crash anymore, so commenting + it out for now... + */ +/* #ifndef __APPLE__ */ + wordfree (&wexp); +/* #endif /\*__APPLE__*\/ */ + return dir; + +# else /*!HAVE_WORDEXP_H*/ +/* E.g. OpenBSD does not have wordexp.h, so we ignore it */ + return path ? g_strdup (path) : NULL; +#endif /*HAVE_WORDEXP_H*/ +} + + +/* note, the g_debugs are commented out because this function may be + * called before the log handler is installed. */ +char* +mu_util_dir_expand (const char *path) +{ + char *dir; + char resolved[PATH_MAX + 1]; + + g_return_val_if_fail (path, NULL); + + dir = do_wordexp (path); + if (!dir) + return NULL; /* error */ + + /* don't try realpath if the dir does not exist */ + if (access (dir, F_OK) != 0) + return dir; + + /* now resolve any symlinks, .. etc. */ + if (realpath (dir, resolved) == NULL) { + /* g_debug ("%s: could not get realpath for '%s': %s", */ + /* __func__, dir, g_strerror(errno)); */ + g_free (dir); + return NULL; + } else + g_free (dir); + + return g_strdup (resolved); +} + +GQuark +mu_util_error_quark (void) +{ + static GQuark error_domain = 0; + + if (G_UNLIKELY(error_domain == 0)) + error_domain = g_quark_from_static_string + ("mu-error-quark"); + + return error_domain; +} + +gboolean +mu_util_check_dir (const gchar* path, gboolean readable, gboolean writeable) +{ + int mode; + struct stat statbuf; + + if (!path) + return FALSE; + + mode = F_OK | (readable ? R_OK : 0) | (writeable ? W_OK : 0); + + if (access (path, mode) != 0) { + /* g_debug ("Cannot access %s: %s", path, g_strerror (errno)); */ + return FALSE; + } + + if (stat (path, &statbuf) != 0) { + /* g_debug ("Cannot stat %s: %s", path, g_strerror (errno)); */ + return FALSE; + } + + return S_ISDIR(statbuf.st_mode) ? TRUE: FALSE; +} + + +gchar* +mu_util_guess_maildir (void) +{ + const gchar *mdir1, *home; + + /* first, try MAILDIR */ + mdir1 = g_getenv ("MAILDIR"); + + if (mdir1 && mu_util_check_dir (mdir1, TRUE, FALSE)) + return g_strdup (mdir1); + + /* then, try /Maildir */ + home = g_get_home_dir(); + if (home) { + char *mdir2; + mdir2 = g_strdup_printf ("%s%cMaildir", + home, G_DIR_SEPARATOR); + if (mu_util_check_dir (mdir2, TRUE, FALSE)) + return mdir2; + g_free (mdir2); + } + + /* nope; nothing found */ + return NULL; +} + +gboolean +mu_util_create_dir_maybe (const gchar *path, mode_t mode, gboolean nowarn) +{ + struct stat statbuf; + + g_return_val_if_fail (path, FALSE); + + /* if it exists, it must be a readable dir */ + if (stat (path, &statbuf) == 0) { + if ((!S_ISDIR(statbuf.st_mode)) || + (access (path, W_OK|R_OK) != 0)) { + if (!nowarn) + g_warning ("not a read-writable" + "directory: %s", path); + return FALSE; + } + } + + if (g_mkdir_with_parents (path, mode) != 0) { + if (!nowarn) + g_warning ("failed to create %s: %s", + path, g_strerror(errno)); + return FALSE; + } + + return TRUE; +} + +gboolean +mu_util_supports (MuFeature feature) +{ + /* check for Guile support */ +#ifndef BUILD_GUILE + if (feature & MU_FEATURE_GUILE) + return FALSE; +#endif /*BUILD_GUILE*/ + + /* check for Gnuplot */ + if (feature & MU_FEATURE_GNUPLOT) + if (!mu_util_program_in_path ("gnuplot")) + return FALSE; + + return TRUE; +} + + +gboolean +mu_util_program_in_path (const char *prog) +{ + gchar *path; + + g_return_val_if_fail (prog, FALSE); + + path = g_find_program_in_path (prog); + g_free (path); + + return (path != NULL) ? TRUE : FALSE; +} + + +/* + * Set the child to a group leader to avoid being killed when the + * parent group is killed. + */ +static void +maybe_setsid (G_GNUC_UNUSED gpointer user_data) +{ +#if HAVE_SETSID + setsid(); +#endif /*HAVE_SETSID*/ +} + +gboolean +mu_util_play (const char *path, GError **err) +{ + GFile *gf; + gboolean rv, is_native; + const gchar *argv[3]; + const char *prog; + + g_return_val_if_fail (path, FALSE); + + gf = g_file_new_for_path(path); + is_native = g_file_is_native(gf); + g_object_unref(gf); + + if (!is_native) { + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_EXECUTE, + "'%s' is not a native file", path); + return FALSE; + } + + prog = g_getenv ("MU_PLAY_PROGRAM"); + if (!prog) { +#ifdef __APPLE__ + prog = "open"; +#else + prog = "xdg-open"; +#endif /*!__APPLE__*/ + } + + if (!mu_util_program_in_path (prog)) { + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_EXECUTE, + "cannot find '%s' in path", prog); + return FALSE; + } + + argv[0] = prog; + argv[1] = path; + argv[2] = NULL; + + err = NULL; + rv = g_spawn_async (NULL, (gchar**)&argv, NULL, + G_SPAWN_SEARCH_PATH, maybe_setsid, + NULL, NULL, err); + return rv; +} + + +unsigned char +mu_util_get_dtype (const char *path, gboolean use_lstat) +{ + int res; + struct stat statbuf; + + g_return_val_if_fail (path, DT_UNKNOWN); + + if (use_lstat) + res = lstat (path, &statbuf); + else + res = stat (path, &statbuf); + + if (res != 0) { + g_warning ("%sstat failed on %s: %s", + use_lstat ? "l" : "", path, g_strerror(errno)); + return DT_UNKNOWN; + } + + /* we only care about dirs, regular files and links */ + if (S_ISREG (statbuf.st_mode)) + return DT_REG; + else if (S_ISDIR (statbuf.st_mode)) + return DT_DIR; + else if (S_ISLNK (statbuf.st_mode)) + return DT_LNK; + + return DT_UNKNOWN; +} + + + +gboolean +mu_util_locale_is_utf8 (void) +{ + const gchar *dummy; + static int is_utf8 = -1; + + if (G_UNLIKELY(is_utf8 == -1)) + is_utf8 = g_get_charset(&dummy) ? 1 : 0; + + return is_utf8 ? TRUE : FALSE; +} + +gboolean +mu_util_fputs_encoded (const char *str, FILE *stream) +{ + int rv; + char *conv; + + g_return_val_if_fail (stream, FALSE); + + /* g_get_charset return TRUE when the locale is UTF8 */ + if (mu_util_locale_is_utf8()) + return fputs (str, stream) == EOF ? FALSE : TRUE; + + /* charset is _not_ utf8, so we need to convert it */ + conv = NULL; + if (g_utf8_validate (str, -1, NULL)) + conv = g_locale_from_utf8 (str, -1, NULL, NULL, NULL); + + /* conversion failed; this happens because is some cases GMime may gives + * us non-UTF-8 strings from e.g. wrongly encoded message-subjects; if + * so, we escape the string */ + conv = conv ? conv : g_strescape (str, "\n\t"); + rv = conv ? fputs (conv, stream) : EOF; + g_free (conv); + + return (rv == EOF) ? FALSE : TRUE; +} + + + +gboolean +mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...) +{ + va_list ap; + char *msg; + + /* don't bother with NULL errors, or errors already set */ + if (!err || *err) + return FALSE; + + msg = NULL; + va_start (ap, frm); + g_vasprintf (&msg, frm, ap); + va_end (ap); + + g_set_error (err, MU_ERROR_DOMAIN, errcode, "%s", msg); + + g_free (msg); + + return FALSE; +} + + + +__attribute__((format(printf, 2, 0))) static gboolean +print_args (FILE *stream, const char *frm, va_list args) +{ + gchar *str; + gboolean rv; + + str = g_strdup_vprintf (frm, args); + + rv = mu_util_fputs_encoded (str, stream); + + g_free (str); + + return rv; +} + + +gboolean +mu_util_print_encoded (const char *frm, ...) +{ + va_list args; + gboolean rv; + + g_return_val_if_fail (frm, FALSE); + + va_start (args, frm); + rv = print_args (stdout, frm, args); + va_end (args); + + return rv; +} + +char* +mu_str_summarize (const char* str, size_t max_lines) +{ + char *summary; + size_t nl_seen; + unsigned i,j; + gboolean last_was_blank; + + g_return_val_if_fail (str, NULL); + g_return_val_if_fail (max_lines > 0, NULL); + + /* len for summary <= original len */ + summary = g_new (gchar, strlen(str) + 1); + + /* copy the string up to max_lines lines, replace CR/LF/tab with + * single space */ + for (i = j = 0, nl_seen = 0, last_was_blank = TRUE; + nl_seen < max_lines && str[i] != '\0'; ++i) { + + if (str[i] == '\n' || str[i] == '\r' || + str[i] == '\t' || str[i] == ' ' ) { + + if (str[i] == '\n') + ++nl_seen; + + /* no double-blanks or blank at end of str */ + if (!last_was_blank && str[i+1] != '\0') + summary[j++] = ' '; + + last_was_blank = TRUE; + } else { + + summary[j++] = str[i]; + last_was_blank = FALSE; + } + } + + summary[j] = '\0'; + return summary; +} diff --git a/lib/utils/mu-util.h b/lib/utils/mu-util.h new file mode 100644 index 0000000..3dedc0a --- /dev/null +++ b/lib/utils/mu-util.h @@ -0,0 +1,349 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema +** +** 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_UTIL_H__ +#define __MU_UTIL_H__ + +#include +#include +#include +#include /* for mode_t */ + +/* hopefully, this should get us a sane PATH_MAX */ +#include +/* not all systems provide PATH_MAX in limits.h */ +#ifndef PATH_MAX +#include +#ifndef PATH_MAX +#define PATH_MAX MAXPATHLEN +#endif /*!PATH_MAX*/ +#endif /*PATH_MAX*/ + +G_BEGIN_DECLS + +/** + * get the expanded path; ie. perform shell expansion on the path. the + * path does not have to exist + * + * @param path path to expand + * + * @return the expanded path as a newly allocated string, or NULL in + * case of error + */ +char* mu_util_dir_expand(const char* path) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * guess the maildir; first try $MAILDIR; if it is unset or + * non-existent, try ~/Maildir if both fail, return NULL + * + * @return full path of the guessed Maildir, or NULL; must be freed (gfree) + */ +char* mu_util_guess_maildir (void) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * if path exists, check that's a read/writeable dir; otherwise try to + * create it (with perms 0700) + * + * @param path path to the dir + * @param mode to set for the dir (as per chmod(1)) + * @param nowarn, if TRUE, don't write warnings (if any) to stderr + * + * @return TRUE if a read/writeable directory `path' exists after + * leaving this function, FALSE otherwise + */ +gboolean mu_util_create_dir_maybe (const gchar *path, mode_t mode, + gboolean nowarn) G_GNUC_WARN_UNUSED_RESULT; + +/** + * check whether path is a directory, and optionally, if it's readable + * and/or writeable + * + * @param path dir path + * @param readable check for readability + * @param writeable check for writability + * + * @return TRUE if dir exist and has the specified properties + */ +gboolean mu_util_check_dir (const gchar* path, gboolean readable, + gboolean writeable) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * is the current locale utf-8 compatible? + * + * @return TRUE if it's utf8 compatible, FALSE otherwise + */ +gboolean mu_util_locale_is_utf8 (void) G_GNUC_CONST; + +/** + * get a 'summary' of the string, ie. the first /n/ lines of the + * strings, with all newlines removed, replaced by single spaces + * + * @param str the source string + * @param max_lines the maximum number of lines to include in the summary + * + * @return a newly allocated string with the summary. use g_free to free it. + */ +char* mu_str_summarize (const char* str, size_t max_lines) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +/** + * write a string (assumed to be in utf8-format) to a stream, + * converted to the current locale + * + * @param str a string + * @param stream a stream + * + * @return TRUE if printing worked, FALSE otherwise + */ +gboolean mu_util_fputs_encoded (const char *str, FILE *stream); + +/** + * print a formatted string (assumed to be in utf8-format) to stdout, + * converted to the current locale + * + * @param a standard printf() format string, followed by a parameter list + * + * @return TRUE if printing worked, FALSE otherwise + */ +gboolean mu_util_print_encoded (const char *frm, ...) G_GNUC_PRINTF(1,2); + + +/** + * Try to 'play' (ie., open with it's associated program) a file. On MacOS, the + * the program 'open' is used for this; on other platforms 'xdg-open' to do the + * actual opening. In addition you can set it to another program by setting the + * MU_PLAY_PROGRAM environment variable + * + * This requires a 'native' file, see g_file_is_native() + * + * @param path full path of the file to open + * @param err receives error information, if any + * + * @return TRUE if it succeeded, FALSE otherwise + */ +gboolean mu_util_play (const char *path, GError **err); + +/** + * Check whether program prog exists in PATH + * + * @param prog a program (executable) + * + * @return TRUE if it exists and is executable, FALSE otherwise + */ +gboolean mu_util_program_in_path (const char *prog); + + +enum _MuFeature { + MU_FEATURE_GUILE = 1 << 0, /* do we support Guile 2.0? */ + MU_FEATURE_GNUPLOT = 1 << 1, /* do we have gnuplot installed? */ +}; +typedef enum _MuFeature MuFeature; + +/** + * Check whether mu supports some particular feature + * + * @param feature a feature (multiple features can be logical-or'd together) + * + * @return TRUE if the feature is supported, FALSE otherwise + */ +gboolean mu_util_supports (MuFeature feature); + + + +/** + * Get an error-query for mu, to be used in `g_set_error'. Recent + * version of Glib warn when using 0 for the error-domain in + * g_set_error. + * + * + * @return an error quark for mu + */ +GQuark mu_util_error_quark (void) G_GNUC_CONST; +#define MU_ERROR_DOMAIN (mu_util_error_quark()) + + +/* + * for OSs with out support for direntry->d_type, like Solaris + */ +#ifndef DT_UNKNOWN +enum { + DT_UNKNOWN = 0, +#define DT_UNKNOWN DT_UNKNOWN + DT_FIFO = 1, +#define DT_FIFO DT_FIFO + DT_CHR = 2, +#define DT_CHR DT_CHR + DT_DIR = 4, +#define DT_DIR DT_DIR + DT_BLK = 6, +#define DT_BLK DT_BLK + DT_REG = 8, +#define DT_REG DT_REG + DT_LNK = 10, +#define DT_LNK DT_LNK + DT_SOCK = 12, +#define DT_SOCK DT_SOCK + DT_WHT = 14 +#define DT_WHT DT_WHT +}; +#endif /*DT_UNKNOWN*/ + + +/** + * get the d_type (as in direntry->d_type) for the file at path, using either + * stat(3) or lstat(3) + * + * @param path full path + * @param use_lstat whether to use lstat (otherwise use stat) + * + * @return DT_REG, DT_DIR, DT_LNK, or DT_UNKNOWN (other values are not supported + * currently ) + */ +unsigned char mu_util_get_dtype (const char *path, gboolean use_lstat); + + +/** + * we need this when using Xapian::Document* from C + * + */ +typedef gpointer XapianDocument; + +/** + * we need this when using Xapian::Enquire* from C + * + */ +typedef gpointer XapianEnquire; + + +/* print a warning for a GError, and free it */ +#define MU_HANDLE_G_ERROR(GE) \ + do { \ + if (!(GE)) \ + g_warning ("%s:%u: an error occurred in %s", \ + __FILE__, __LINE__, __func__); \ + else { \ + g_warning ("error %u: %s", (GE)->code, (GE)->message); \ + g_error_free ((GE)); \ + } \ + } while (0) + + +#define MU_G_ERROR_CODE(GE) ((GE)&&(*(GE))?(MuError)(*(GE))->code:MU_ERROR) + + +enum _MuError { + /* no error at all! */ + MU_OK = 0, + + /* generic error */ + MU_ERROR = 1, + MU_ERROR_IN_PARAMETERS = 2, + MU_ERROR_INTERNAL = 3, + MU_ERROR_NO_MATCHES = 4, + + /* not really an error; for callbacks */ + MU_IGNORE = 5, + + MU_ERROR_SCRIPT_NOT_FOUND = 8, + + /* general xapian related error */ + MU_ERROR_XAPIAN = 11, + + /* (parsing) error in the query */ + MU_ERROR_XAPIAN_QUERY = 13, + + /* missing data for a document */ + MU_ERROR_XAPIAN_MISSING_DATA = 17, + /* can't get write lock */ + MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK = 19, + /* could not write */ + MU_ERROR_XAPIAN_STORE_FAILED = 21, + /* could not remove */ + MU_ERROR_XAPIAN_REMOVE_FAILED = 22, + /* database was modified; reload */ + MU_ERROR_XAPIAN_MODIFIED = 23, + /* database was modified; reload */ + MU_ERROR_XAPIAN_NEEDS_REINDEX = 24, + /* database schema version doesn't match */ + MU_ERROR_XAPIAN_SCHEMA_MISMATCH = 25, + /* failed to open the database */ + MU_ERROR_XAPIAN_CANNOT_OPEN = 26, + + /* GMime related errors */ + + /* gmime parsing related error */ + MU_ERROR_GMIME = 30, + + /* contacts related errors */ + MU_ERROR_CONTACTS = 50, + MU_ERROR_CONTACTS_CANNOT_RETRIEVE = 51, + + /* crypto related errors */ + MU_ERROR_CRYPTO = 60, + + + /* File errors */ + /* generic file-related error */ + MU_ERROR_FILE = 70, + MU_ERROR_FILE_INVALID_NAME = 71, + MU_ERROR_FILE_CANNOT_LINK = 72, + MU_ERROR_FILE_CANNOT_OPEN = 73, + MU_ERROR_FILE_CANNOT_READ = 74, + MU_ERROR_FILE_CANNOT_EXECUTE = 75, + MU_ERROR_FILE_CANNOT_CREATE = 76, + MU_ERROR_FILE_CANNOT_MKDIR = 77, + MU_ERROR_FILE_STAT_FAILED = 78, + MU_ERROR_FILE_READDIR_FAILED = 79, + MU_ERROR_FILE_INVALID_SOURCE = 80, + MU_ERROR_FILE_TARGET_EQUALS_SOURCE = 81, + MU_ERROR_FILE_CANNOT_WRITE = 82, + MU_ERROR_FILE_CANNOT_UNLINK = 83, + + /* not really an error, used in callbacks */ + MU_STOP = 99 +}; +typedef enum _MuError MuError; + + +/** + * set an error if it's not already set, and return FALSE + * + * @param err errptr, or NULL + * @param errcode error code + * @param frm printf-style format, followed by parameters + * + * @return FALSE + */ +gboolean mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...) + G_GNUC_PRINTF(3,4); + +#define MU_COLOR_RED "\x1b[31m" +#define MU_COLOR_GREEN "\x1b[32m" +#define MU_COLOR_YELLOW "\x1b[33m" +#define MU_COLOR_BLUE "\x1b[34m" +#define MU_COLOR_MAGENTA "\x1b[35m" +#define MU_COLOR_CYAN "\x1b[36m" +#define MU_COLOR_DEFAULT "\x1b[0m" + +G_END_DECLS + +#endif /*__MU_UTIL_H__*/ diff --git a/lib/utils/mu-utils-format.hh b/lib/utils/mu-utils-format.hh new file mode 100644 index 0000000..3ee0bcd --- /dev/null +++ b/lib/utils/mu-utils-format.hh @@ -0,0 +1,61 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_UTILS_FORMAT_HH__ +#define MU_UTILS_FORMAT_HH__ + +#include +#include + +namespace Mu { + +/** + * Quote & escape a string for " and \ + * + * @param str a string + * + * @return quoted string + */ +std::string quote(const std::string& str); + +/** + * Format a string, printf style + * + * @param frm format string + * @param ... parameters + * + * @return a formatted string + */ +std::string format(const char* frm, ...) __attribute__((format(printf, 1, 2))); + +/** + * Format a string, printf style + * + * @param frm format string + * @param ... parameters + * + * @return a formatted string + */ +std::string vformat(const char* frm, va_list args) __attribute__((format(printf, 1, 0))); + + +} // namepace Mu + + +#endif /* MU_UTILS_FORMAT_HH__ */ diff --git a/lib/utils/mu-utils.cc b/lib/utils/mu-utils.cc new file mode 100644 index 0000000..6a88788 --- /dev/null +++ b/lib/utils/mu-utils.cc @@ -0,0 +1,638 @@ +/* +** Copyright (C) 2017-2022 Dirk-Jan C. Binnema +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE +#include +#endif /*_XOPEN_SOURCE*/ + +#include + +#include + +#define GNU_SOURCE +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "mu-utils.hh" +#include "mu-utils-format.hh" +#include "mu-util.h" +#include "mu-error.hh" +#include "mu-option.hh" + +using namespace Mu; + +namespace { + +static gunichar +unichar_tolower(gunichar uc) +{ + if (!g_unichar_isalpha(uc)) + return uc; + + if (g_unichar_get_script(uc) != G_UNICODE_SCRIPT_LATIN) + return g_unichar_tolower(uc); + + switch (uc) { + case 0x00e6: + case 0x00c6: return 'e'; /* æ */ + case 0x00f8: return 'o'; /* ø */ + case 0x0110: + case 0x0111: + return 'd'; /* đ */ + /* todo: many more */ + default: return g_unichar_tolower(uc); + } +} + +/** + * gx_utf8_flatten: + * @str: a UTF-8 string + * @len: the length of @str, or -1 if it is %NULL-terminated + * + * Flatten some UTF-8 string; that is, downcase it and remove any diacritics. + * + * Returns: (transfer full): a flattened string, free with g_free(). + */ +static char* +gx_utf8_flatten(const gchar* str, gssize len) +{ + GString* gstr; + char * norm, *cur; + + g_return_val_if_fail(str, NULL); + + norm = g_utf8_normalize(str, len, G_NORMALIZE_ALL); + if (!norm) + return NULL; + + gstr = g_string_sized_new(strlen(norm)); + + for (cur = norm; cur && *cur; cur = g_utf8_next_char(cur)) { + gunichar uc; + + uc = g_utf8_get_char(cur); + if (g_unichar_combining_class(uc) != 0) + continue; + + g_string_append_unichar(gstr, unichar_tolower(uc)); + } + + g_free(norm); + + return g_string_free(gstr, FALSE); +} + +} // namespace + +std::string // gx_utf8_flatten +Mu::utf8_flatten(const char* str) +{ + if (!str) + return {}; + + // the pure-ascii case + if (g_str_is_ascii(str)) { + auto l = g_ascii_strdown(str, -1); + std::string s{l}; + g_free(l); + return s; + } + + // seems we need the big guns + char* flat = gx_utf8_flatten(str, -1); + if (!flat) + return {}; + + std::string s{flat}; + g_free(flat); + + return s; +} + + +/* turn \0-terminated buf into ascii (which is a utf8 subset); convert + * any non-ascii into '.' + */ +static char* +asciify_in_place (char *buf) +{ + char *c; + + g_return_val_if_fail (buf, NULL); + + for (c = buf; c && *c; ++c) { + if ((!isprint(*c) && !isspace (*c)) || !isascii(*c)) + *c = '.'; + } + + return buf; +} + +static char* +utf8ify (const char *buf) +{ + char *utf8; + + g_return_val_if_fail (buf, NULL); + + utf8 = g_strdup (buf); + + if (!g_utf8_validate (buf, -1, NULL)) + asciify_in_place (utf8); + + return utf8; +} + + +std::string +Mu::utf8_clean(const std::string& dirty) +{ + g_autoptr(GString) gstr = g_string_sized_new(dirty.length()); + g_autofree char *cstr = utf8ify(dirty.c_str()); + + for (auto cur = cstr; cur && *cur; cur = g_utf8_next_char(cur)) { + const gunichar uc = g_utf8_get_char(cur); + if (g_unichar_iscntrl(uc)) + g_string_append_c(gstr, ' '); + else + g_string_append_unichar(gstr, uc); + } + + return std::string{g_strstrip(gstr->str)}; +} + +std::string +Mu::remove_ctrl(const std::string& str) +{ + char prev{'\0'}; + std::string result; + result.reserve(str.length()); + + for (auto&& c : str) { + if (::iscntrl(c) || c == ' ') { + if (prev != ' ') + result += prev = ' '; + } else + result += prev = c; + } + + return result; +} + +std::vector +Mu::split(const std::string& str, const std::string& sepa) +{ + std::vector vec; + size_t b = 0, e = 0; + + /* special cases */ + if (str.empty()) + return vec; + else if (sepa.empty()) { + for (auto&& c: str) + vec.emplace_back(1, c); + return vec; + } + + while (true) { + if (e = str.find(sepa, b); e != std::string::npos) { + vec.emplace_back(str.substr(b, e - b)); + b = e + sepa.length(); + } else { + vec.emplace_back(str.substr(b)); + break; + } + } + + return vec; +} + +std::vector +Mu::split(const std::string& str, char sepa) +{ + std::vector vec; + size_t b = 0, e = 0; + + /* special case */ + if (str.empty()) + return vec; + + while (true) { + if (e = str.find(sepa, b); e != std::string::npos) { + vec.emplace_back(str.substr(b, e - b)); + b = e + sizeof(sepa); + } else { + vec.emplace_back(str.substr(b)); + break; + } + } + + return vec; +} + +std::vector +Mu::split(const std::string& str, const std::regex& sepa_rx) +{ + std::sregex_token_iterator it(str.begin(), str.end(), sepa_rx, -1); + std::sregex_token_iterator end; + + return {it, end}; +} + +std::string +Mu::join(const std::vector& svec, const std::string& sepa) +{ + if (svec.empty()) + return {}; + + + /* calculate the overall size beforehand, to avoid re-allocations. */ + size_t value_len = + std::accumulate(svec.cbegin(), svec.cend(), 0, + [](size_t size, const std::string& s) { + return size + s.size(); + }) + (svec.size() - 1) * sepa.length(); + + std::string value; + value.reserve(value_len); + + std::accumulate(svec.cbegin(), svec.cend(), std::ref(value), + [&](std::string& s1, const std::string& s2)->std::string& { + if (s1.empty()) + s1 = s2; + else { + s1.append(sepa); + s1.append(s2); + } + return s1; + }); + + return value; +} + +std::string +Mu::quote(const std::string& str) +{ + std::string res{"\""}; + + for (auto&& k : str) { + switch (k) { + case '"': res += "\\\""; break; + case '\\': res += "\\\\"; break; + default: res += k; + } + } + + return res + "\""; +} + +std::string +Mu::format(const char* frm, ...) +{ + va_list args; + + va_start(args, frm); + auto str = vformat(frm, args); + va_end(args); + + return str; +} + +std::string +Mu::vformat(const char* frm, va_list args) +{ + char* s{}; + const auto res = g_vasprintf(&s, frm, args); + if (res == -1) { + std::cerr << "string format failed" << std::endl; + return {}; + } + + std::string str{s}; + g_free(s); + + return str; +} + +std::string +Mu::time_to_string(const char *frm, time_t t, bool utc) +{ + g_return_val_if_fail(frm, ""); + + GDateTime* dt = std::invoke([&] { + if (utc) + return g_date_time_new_from_unix_utc(t); + else + return g_date_time_new_from_unix_local(t); + }); + + if (!dt) { + g_warning("time_t out of range: <%" G_GUINT64_FORMAT ">", + static_cast(t)); + return {}; + } + + frm = frm ? frm : "%c"; + auto datestr{to_string_opt_gchar(g_date_time_format(dt, frm))}; + g_date_time_unref(dt); + if (!datestr) + g_warning("failed to format time with format '%s'", frm); + + return datestr.value_or(""); +} + +static Option +delta_ymwdhMs(const std::string& expr) +{ + char* endptr; + auto num = strtol(expr.c_str(), &endptr, 10); + if (num <= 0 || num > 9999 || !endptr || !*endptr) + return Nothing; + + int years, months, weeks, days, hours, minutes, seconds; + years = months = weeks = days = hours = minutes = seconds = 0; + + switch (endptr[0]) { + case 's': seconds = num; break; + case 'M': minutes = num; break; + case 'h': hours = num; break; + case 'd': days = num; break; + case 'w': weeks = num; break; + case 'm': months = num; break; + case 'y': years = num; break; + default: + return Nothing; + } + + GDateTime *then, *now = g_date_time_new_now_local(); + if (weeks != 0) + then = g_date_time_add_weeks(now, -weeks); + else + then = + g_date_time_add_full(now, -years, -months, -days, -hours, -minutes, -seconds); + + auto t = std::max(0, g_date_time_to_unix(then)); + + g_date_time_unref(then); + g_date_time_unref(now); + + return t; +} + +static Option +special_date_time(const std::string& d, bool is_first) +{ + if (d == "now") + return ::time({}); + + if (d == "today") { + GDateTime *dt, *midnight; + dt = g_date_time_new_now_local(); + + if (!is_first) { + GDateTime* tmp = dt; + dt = g_date_time_add_days(dt, 1); + g_date_time_unref(tmp); + } + + midnight = g_date_time_add_full(dt, + 0, + 0, + 0, + -g_date_time_get_hour(dt), + -g_date_time_get_minute(dt), + -g_date_time_get_second(dt)); + time_t t = MAX(0, (gint64)g_date_time_to_unix(midnight)); + g_date_time_unref(dt); + g_date_time_unref(midnight); + + return t; + } + + return Nothing; +} + +// if a date has a month day greater than the number of days in that month, +// change it to a valid date point to the last second in that month +static void +fixup_month(struct tm* tbuf) +{ + decltype(tbuf->tm_mday) max_days; + const auto month = tbuf->tm_mon + 1; + const auto year = tbuf->tm_year + 1900; + + switch (month) { + case 2: + if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) + max_days = 29; + else + max_days = 28; + break; + case 4: + case 6: + case 9: + case 11: + max_days = 30; + break; + default: + max_days = 31; + break; + } + + if (tbuf->tm_mday > max_days) { + tbuf->tm_mday = max_days; + tbuf->tm_hour = 23; + tbuf->tm_min = 59; + tbuf->tm_sec = 59; + } + } + + +Option +Mu::parse_date_time(const std::string& dstr, bool is_first) +{ + struct tm tbuf{}; + GDateTime *dtime{}; + int64_t t; + + /* one-sided dates */ + if (dstr.empty()) + return is_first ? 0 : G_MAXINT64; + else if (dstr == "today" || dstr == "now") + return special_date_time(dstr, is_first); + else if (dstr.find_first_of("ymdwhMs") != std::string::npos) + return delta_ymwdhMs(dstr); + + constexpr char UserDateMin[] = "19700101000000"; + constexpr char UserDateMax[] = "29991231235959"; + + std::string date(is_first ? UserDateMin : UserDateMax); + std::copy_if(dstr.begin(), dstr.end(), date.begin(), [](auto c) { return isdigit(c); }); + + if (!::strptime(date.c_str(), "%Y%m%d%H%M%S", &tbuf) && + !::strptime(date.c_str(), "%Y%m%d%H%M", &tbuf) && + !::strptime(date.c_str(), "%Y%m%d%H", &tbuf) && + !::strptime(date.c_str(), "%Y%m%d", &tbuf) && + !::strptime(date.c_str(), "%Y%m", &tbuf) && + !::strptime(date.c_str(), "%Y", &tbuf)) + return Nothing; + + fixup_month(&tbuf); + dtime = g_date_time_new_local(tbuf.tm_year + 1900, + tbuf.tm_mon + 1, + tbuf.tm_mday, + tbuf.tm_hour, + tbuf.tm_min, + tbuf.tm_sec); + t = g_date_time_to_unix(dtime); + g_date_time_unref(dtime); + + return std::max(t, 0); +} + + +Option +Mu::parse_size(const std::string& val, bool is_first) +{ + int64_t size{-1}; + std::string str; + GRegex* rx; + GMatchInfo* minfo; + + /* one-sided ranges */ + if (val.empty()) + return is_first ? 0 : std::numeric_limits::max(); + + rx = g_regex_new("^(\\d+)(b|k|kb|m|mb|g|gb)?$", + G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL); + minfo = NULL; + if (g_regex_match(rx, val.c_str(), (GRegexMatchFlags)0, &minfo)) { + + char* s; + s = g_match_info_fetch(minfo, 1); + size = atoll(s); + g_free(s); + + s = g_match_info_fetch(minfo, 2); + switch (s ? g_ascii_tolower(s[0]) : 0) { + case 'k': size *= 1024; break; + case 'm': size *= (1024 * 1024); break; + case 'g': size *= (1024 * 1024 * 1024); break; + default: break; + } + + g_free(s); + } + + g_regex_unref(rx); + g_match_info_unref(minfo); + + if (size < 0) + return Nothing; + else + return size; + +} + +std::string +Mu::to_lexnum(int64_t val) +{ + char buf[18]; /* 1 byte prefix + hex + \0 */ + buf[0] = 'f' + ::snprintf(buf + 1, sizeof(buf) - 1, "%" PRIx64, val); + return buf; +} + +int64_t +Mu::from_lexnum(const std::string& str) +{ + int64_t val{}; + std::from_chars(str.c_str() + 1, str.c_str() + str.size(), val, 16); + + return val; +} + + +std::string +Mu::canonicalize_filename(const std::string& path, const std::string& relative_to) +{ + auto str{to_string_opt_gchar( + g_canonicalize_filename( + path.c_str(), + relative_to.empty() ? nullptr : relative_to.c_str())).value()}; + + // remove trailing '/'... is this needed? + if (str[str.length()-1] == G_DIR_SEPARATOR) + str.erase(str.length() - 1); + + return str; +} + + +bool +Mu::locale_workaround() try +{ + // quite horrible... but some systems break otherwise with + // https://github.com/djcb/mu/issues/2252 + + try { + std::locale::global(std::locale("")); + } catch (const std::runtime_error& re) { + g_setenv("LC_ALL", "C", 1); + std::locale::global(std::locale("")); + } + + return true; + +} catch (...) { + return false; +} + +bool +Mu::timezone_available(const std::string& tz) +{ + const auto old_tz = g_getenv("TZ"); + + g_setenv("TZ", tz.c_str(), TRUE); + + auto tzone = g_time_zone_new_local (); + bool have_tz = g_strcmp0(g_time_zone_get_identifier(tzone), tz.c_str()) == 0; + g_time_zone_unref (tzone); + + if (old_tz) + g_setenv("TZ", old_tz, TRUE); + else + g_unsetenv("TZ"); + + return have_tz; +} diff --git a/lib/utils/mu-utils.hh b/lib/utils/mu-utils.hh new file mode 100644 index 0000000..4a9f70d --- /dev/null +++ b/lib/utils/mu-utils.hh @@ -0,0 +1,459 @@ +/* +** Copyright (C) 2020-2022 Dirk-Jan C. Binnema +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef __MU_UTILS_HH__ +#define __MU_UTILS_HH__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mu-utils-format.hh" +#include "mu-option.hh" + +namespace Mu { + +using StringVec = std::vector; + +/** + * Flatten a string -- downcase and fold diacritics etc. + * + * @param str a string + * + * @return a flattened string + */ +std::string utf8_flatten(const char* str); +inline std::string +utf8_flatten(const std::string& s) +{ + return utf8_flatten(s.c_str()); +} + +/** + * Replace all control characters with spaces, and remove leading and trailing space. + * + * @param dirty an unclean string + * + * @return a cleaned-up string. + */ +std::string utf8_clean(const std::string& dirty); + +/** + * Remove ctrl characters, replacing them with ' '; subsequent + * ctrl characters are replaced by a single ' ' + * + * @param str a string + * + * @return the string without control characters + */ +std::string remove_ctrl(const std::string& str); + +/** + * Split a string in parts. As a special case, splitting an empty string + * yields an empty vector (not a vector with a single empty element) + * + * @param str a string + * @param sepa the separator + * + * @return the parts. + */ +std::vector split(const std::string& str, const std::string& sepa); + +/** + * Split a string in parts. As a special case, splitting an empty string + * yields an empty vector (not a vector with a single empty element) + * + * @param str a string + * @param sepa the separator + * + * @return the parts. + */ +std::vector split(const std::string& str, char sepa); + +/** + * Split a string in parts + * + * @param str a string + * @param sepa the separator regex + * + * @return the parts. + */ +std::vector split(const std::string& str, const std::regex& sepa_rx); + +/** + * Join the strings in svec into a string, separated by sepa + * + * @param svec a string vector + * @param sepa separator + * + * @return string + */ +std::string join(const std::vector& svec, const std::string& sepa); +static inline std::string join(const std::vector& svec, char sepa) { + return join(svec, std::string(1, sepa)); +} + +/** + * Parse a date string to the corresponding time_t + * * + * @param date the date expressed a YYYYMMDDHHMMSS or any n... of the first + * characters, using the local timezone. + * @param first whether to fill out incomplete dates to the start or the end; + * ie. either 1972 -> 197201010000 or 1972 -> 197212312359 + * + * @return the corresponding time_t or Nothing if parsing failed. + */ +Option parse_date_time(const std::string& date, bool first); + +/** + * 64-bit incarnation of time_t expressed as a 10-digit string. Uses 64-bit for the time-value, + * regardless of the size of time_t. + * + * @param t some time value + * + * @return + */ +std::string date_to_time_t_string(int64_t t); + +/** + * Get a string for a given time_t and format + * memory that must be freed after use. + * + * @param frm the format of the string (in strftime(3) format) + * @param t the time as time_t + * @param utc whether to display as UTC(if true) or local time + * + * @return a string representation of the time in UTF8-format, or empty in case + * of error. + */ +std::string time_to_string(const char *frm, time_t t, bool utc = false) G_GNUC_CONST; + + +/** + * Hack to avoid locale crashes + * + * @return true if setting locale worked; false otherwise + */ +bool locale_workaround(); + + +/** + * Is the given timezone available? For tests + * + * @param tz a timezone, such as Europe/Helsinki + * + * @return true or false + */ +bool timezone_available(const std::string& tz); + + + +// https://stackoverflow.com/questions/19053351/how-do-i-use-a-custom-deleter-with-a-stdunique-ptr-member +template +struct deleter_from_fn { + template + constexpr void operator()(T* arg) const { + fn(arg); + } +}; +template +using deletable_unique_ptr = std::unique_ptr>; + + + +using Clock = std::chrono::steady_clock; +using Duration = Clock::duration; + +template +constexpr int64_t +to_unit(Duration d) +{ + using namespace std::chrono; + return duration_cast(d).count(); +} + +constexpr int64_t +to_s(Duration d) +{ + return to_unit(d); +} +constexpr int64_t +to_ms(Duration d) +{ + return to_unit(d); +} +constexpr int64_t +to_us(Duration d) +{ + return to_unit(d); +} + +struct StopWatch { + using Clock = std::chrono::steady_clock; + StopWatch(const std::string name) : start_{Clock::now()}, name_{name} {} + ~StopWatch() + { + const auto us{static_cast(to_us(Clock::now() - start_))}; + if (us > 2000000) + g_debug("%s: finished after %0.1f s", name_.c_str(), us / 1000000); + else if (us > 2000) + g_debug("%s: finished after %0.1f ms", name_.c_str(), us / 1000); + else + g_debug("%s: finished after %g us", name_.c_str(), us); + } + +private: + Clock::time_point start_; + std::string name_; +}; + +/** + * See g_canonicalize_filename + * + * @param filename + * @param relative_to + * + * @return + */ +std::string canonicalize_filename(const std::string& path, const std::string& relative_to); + +/** + * Convert a size string to a size in bytes + * + * @param sizestr the size string + * @param first + * + * @return the size or Nothing if parsing failed + */ +Option parse_size(const std::string& sizestr, bool first); + +/** + * Convert a size into a size in bytes string + * + * @param size the size + * @param first + * + * @return the size expressed as a string with the decimal number of bytes + */ +std::string size_to_string(int64_t size); + +/** + * Convert any ostreamable<< value to a string + * + * @param t the value + * + * @return a std::string + */ +template +static inline std::string +to_string(const T& val) +{ + std::stringstream sstr; + sstr << val; + + return sstr.str(); +} + +/** + * Consume a gchar and return a std::string + * + * @param str a gchar* (consumed/freed) + * + * @return a std::string, empty if gchar was {} + */ +static inline std::string +to_string_gchar(gchar*&& str) +{ + std::string s(str?str:""); + g_free(str); + return s; +} + + +/* + * Lexicals Number are lexicographically sortable string representations + * of numbers. Start with 'g' + length of number in hex, followed by + * the ascii for the hex represntation. So, + * + * 0 -> 'g0' + * 1 -> 'g1' + * 10 -> 'ga' + * 16 -> 'h10' + * + * etc. + */ +std::string to_lexnum(int64_t val); +int64_t from_lexnum(const std::string& str); + +/** + * Like std::find_if, but using sequence instead of a range. + * + * @param seq some std::find_if compatible sequence + * @param pred a predicate + * + * @return an iterator + */ +template +typename Sequence::const_iterator seq_find_if(const Sequence& seq, UnaryPredicate pred) { + return std::find_if(seq.cbegin(), seq.cend(), pred); +} + +/** + * Is at least pred(element) true for at least one element of sequence + * + * @param seq sequence + * @param pred a predicate + * + * @return true or false + */ +template +bool seq_some(const Sequence& seq, UnaryPredicate pred) { + return seq_find_if(seq, pred) != seq.cend(); +} + +/** + * Create a sequence that has all element of seq for which pred is true + * + * @param seq sequence + * @param pred false + * + * @return sequence + */ +template +Sequence seq_filter(const Sequence& seq, UnaryPredicate pred) { + Sequence res; + std::copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred); + return res; +} + +/** + * Create a sequence that has all element of seq for which pred is false + * + * @param seq sequence + * @param pred false + * + * @return sequence + */ +template +Sequence seq_remove(const Sequence& seq, UnaryPredicate pred) { + Sequence res; + std::remove_copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred); + return res; +} + +template +void seq_sort(Sequence& seq, Compare cmp) { std::sort(seq.begin(), seq.end(), cmp); } + + +/** + * Like std::accumulate, but using a sequence instead of a range. + * + * @param seq some std::accumulate compatible sequence + * @param init the initial value + * @param op binary operation to calculate the next element + * + * @return the result value. + */ +template +ResultType seq_fold(const Sequence& seq, ResultType init, BinaryOp op) { + return std::accumulate(seq.cbegin(), seq.cend(), init, op); +} + +template +void seq_for_each(const Sequence& seq, UnaryOp op) { + std::for_each(seq.cbegin(), seq.cend(), op); +} + + +/** + * Convert string view in something printable with %*s + */ +#define STR_V(sv__) static_cast((sv__).size()), (sv__).data() + +struct MaybeAnsi { + explicit MaybeAnsi(bool use_color) : color_{use_color} {} + + enum struct Color { + Black = 30, + Red = 31, + Green = 32, + Yellow = 33, + Blue = 34, + Magenta = 35, + Cyan = 36, + White = 37, + + BrightBlack = 90, + BrightRed = 91, + BrightGreen = 92, + BrightYellow = 93, + BrightBlue = 94, + BrightMagenta = 95, + BrightCyan = 96, + BrightWhite = 97, + }; + + std::string fg(Color c) const { return ansi(c, true); } + std::string bg(Color c) const { return ansi(c, false); } + + std::string reset() const { return color_ ? "\x1b[0m" : ""; } + +private: + std::string ansi(Color c, bool fg = true) const + { + return color_ ? format("\x1b[%dm", static_cast(c) + (fg ? 0 : 10)) : ""; + } + + const bool color_; +}; + +/// Allow using enum structs as bitflags +#define MU_TO_NUM(ET, ELM) std::underlying_type_t(ELM) +#define MU_TO_ENUM(ET, NUM) static_cast(NUM) +#define MU_ENABLE_BITOPS(ET) \ + constexpr ET operator&(ET e1, ET e2) \ + { \ + return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) & MU_TO_NUM(ET, e2)); \ + } \ + constexpr ET operator|(ET e1, ET e2) \ + { \ + return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) | MU_TO_NUM(ET, e2)); \ + } \ + constexpr ET operator~(ET e) { return MU_TO_ENUM(ET, ~(MU_TO_NUM(ET, e))); } \ + constexpr bool any_of(ET e) { return MU_TO_NUM(ET, e) != 0; } \ + constexpr bool none_of(ET e) { return MU_TO_NUM(ET, e) == 0; } \ + constexpr bool one_of(ET e1, ET e2) { return (e1 & e2) == e2; } \ + constexpr ET& operator&=(ET& e1, ET e2) { return e1 = e1 & e2; } \ + constexpr ET& operator|=(ET& e1, ET e2) { return e1 = e1 | e2; } \ + static_assert(1==1) // require a semicolon + +} // namespace Mu + +#endif /* __MU_UTILS_HH__ */ diff --git a/lib/utils/mu-xapian-utils.hh b/lib/utils/mu-xapian-utils.hh new file mode 100644 index 0000000..12f3c2c --- /dev/null +++ b/lib/utils/mu-xapian-utils.hh @@ -0,0 +1,86 @@ +/* +** Copyright (C) 2021 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_XAPIAN_UTILS_HH__ +#define MU_XAPIAN_UTILS_HH__ + +#include +#include +#include "mu-result.hh" + +namespace Mu { + +// LCOV_EXCL_START + +// avoid exception-handling boilerplate. +template +void +xapian_try(Func&& func) noexcept +try { + func(); +} catch (const Xapian::Error& xerr) { + g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str()); +} catch (const std::runtime_error& re) { + g_critical("%s: error: %s", __func__, re.what()); +} catch (const std::exception& e) { + g_critical("%s: caught exception: %s", __func__, e.what()); +} catch (...) { + g_critical("%s: caught exception", __func__); +} + +template > +auto +xapian_try(Func&& func, Default&& def) noexcept -> std::decay_t +try { + return func(); +} catch (const Xapian::Error& xerr) { + g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str()); + return static_cast(def); +} catch (const std::runtime_error& re) { + g_critical("%s: error: %s", __func__, re.what()); + return static_cast(def); +} catch (const std::exception& e) { + g_critical("%s: caught exception: %s", __func__, e.what()); + return static_cast(def); +} catch (...) { + g_critical("%s: caught exception", __func__); + return static_cast(def); +} + + +template +auto +xapian_try_result(Func&& func) noexcept -> std::decay_t +try { + return func(); +} catch (const Xapian::Error& xerr) { + return Err(Error::Code::Xapian, "%s", xerr.get_error_string()); +} catch (const std::runtime_error& re) { + return Err(Error::Code::Internal, "runtime error: %s", re.what()); +} catch (const std::exception& e) { + return Err(Error::Code::Internal, "caught exception: %s", e.what()); +} catch (...) { + return Err(Error::Code::Internal, "caught exception"); +} + +// LCOV_EXCL_STOP + +} // namespace Mu + +#endif /* MU_ XAPIAN_UTILS_HH__ */ diff --git a/lib/utils/tests/meson.build b/lib/utils/tests/meson.build new file mode 100644 index 0000000..85dc3c5 --- /dev/null +++ b/lib/utils/tests/meson.build @@ -0,0 +1,45 @@ +## Copyright (C) 2021 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +################################################################################ +# tests +# +test('test-command-parser', + executable('test-command-parser', + 'test-command-parser.cc', + install: false, + dependencies: [glib_dep, lib_mu_utils_dep])) +test('test-mu-util', + executable('test-mu-util', + 'test-mu-util.c', + install: false, + dependencies: [glib_dep,config_h_dep, lib_mu_utils_dep])) +test('test-option', + executable('test-option', + 'test-option.cc', + install: false, + dependencies: [glib_dep, lib_mu_utils_dep])) +test('test-mu-utils', + executable('test-mu-utils', + 'test-utils.cc', + install: false, + dependencies: [glib_dep, lib_mu_utils_dep])) +test('test-sexp', + executable('test-sexp', + 'test-sexp.cc', + install: false, + dependencies: [glib_dep, lib_mu_utils_dep] )) diff --git a/lib/utils/tests/test-command-parser.cc b/lib/utils/tests/test-command-parser.cc new file mode 100644 index 0000000..4156b03 --- /dev/null +++ b/lib/utils/tests/test-command-parser.cc @@ -0,0 +1,149 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include +#include + +#include "mu-command-parser.hh" +#include "mu-utils.hh" +#include "mu-test-utils.hh" + +using namespace Mu; + +static void +test_param_getters() +{ + const auto sexp{Sexp::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")}; + + if (g_test_verbose()) + std::cout << sexp << "\n"; + + g_assert_cmpint(Command::get_int_or(sexp.list(), ":bar"), ==, 123); + assert_equal(Command::get_string_or(sexp.list(), ":bra", "bla"), "bla"); + assert_equal(Command::get_string_or(sexp.list(), ":cuux"), "456"); + + g_assert_true(Command::get_bool_or(sexp.list(), ":boo") == false); + g_assert_true(Command::get_bool_or(sexp.list(), ":bah") == true); +} + +static bool +call(const Command::CommandMap& cmap, const std::string& str) +try { + const auto sexp{Sexp::make_parse(str)}; + invoke(cmap, sexp); + + return true; + +} catch (const Error& err) { + g_warning("%s", err.what()); + return false; +} + +static void +test_command() +{ + using namespace Command; + allow_warnings(); + + CommandMap cmap; + + cmap.emplace( + "my-command", + CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}}, + {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}}, + "My command,", + {}}); + + g_assert_true(call(cmap, "(my-command :param1 \"hello\")")); + g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)")); + + g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)")); +} + +static void +test_command2() +{ + using namespace Command; + allow_warnings(); + + CommandMap cmap; + cmap.emplace("bla", + CommandInfo{ArgMap{ + {":foo", ArgInfo{Sexp::Type::Number, false, "foo"}}, + {":bar", ArgInfo{Sexp::Type::String, false, "bar"}}, + }, + "yeah", + [&](const auto& params) {}}); + + g_assert_true(call(cmap, "(bla :foo nil)")); + g_assert_false(call(cmap, "(bla :foo nil :bla nil)")); +} + +static void +test_command_fail() +{ + using namespace Command; + + allow_warnings(); + + CommandMap cmap; + + cmap.emplace( + "my-command", + CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}}, + {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}}, + "My command,", + {}}); + + g_assert_false(call(cmap, "(my-command)")); + g_assert_false(call(cmap, "(my-command2)")); + g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)")); + g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")")); +} + +static void +black_hole() +{ +} + +int +main(int argc, char* argv[]) try { + + mu_test_init(&argc, &argv); + + g_test_add_func("/utils/command-parser/param-getters", test_param_getters); + g_test_add_func("/utils/command-parser/command", test_command); + g_test_add_func("/utils/command-parser/command2", test_command2); + g_test_add_func("/utils/command-parser/command-fail", test_command_fail); + + g_log_set_handler( + NULL, + (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), + (GLogFunc)black_hole, + NULL); + + return g_test_run(); + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} diff --git a/lib/utils/tests/test-mu-str.c b/lib/utils/tests/test-mu-str.c new file mode 100644 index 0000000..f714d3f --- /dev/null +++ b/lib/utils/tests/test-mu-str.c @@ -0,0 +1,169 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include +#include +#include + +#include + +#include "mu-str.h" + +static void +assert_cmplst (GSList *lst, const char *items[]) +{ + int i; + + if (!lst) + g_assert (!items); + + for (i = 0; lst; lst = g_slist_next(lst), ++i) + g_assert_cmpstr ((char*)lst->data,==,items[i]); + + g_assert (items[i] == NULL); +} + + +static GSList* +create_list (const char *items[]) +{ + GSList *lst; + + lst = NULL; + while (items && *items) { + lst = g_slist_prepend (lst, g_strdup(*items)); + ++items; + } + + return g_slist_reverse (lst); + +} + +static void +test_mu_str_from_list (void) +{ + { + const char *strs[] = {"aap", "noot", "mies", NULL}; + GSList *lst = create_list (strs); + gchar *str = mu_str_from_list (lst, ','); + g_assert_cmpstr ("aap,noot,mies", ==, str); + mu_str_free_list (lst); + g_free (str); + } + + { + const char *strs[] = {"aap", "no,ot", "mies", NULL}; + GSList *lst = create_list (strs); + gchar *str = mu_str_from_list (lst, ','); + g_assert_cmpstr ("aap,no,ot,mies", ==, str); + mu_str_free_list (lst); + g_free (str); + } + + { + const char *strs[] = {NULL}; + GSList *lst = create_list (strs); + gchar *str = mu_str_from_list (lst,'@'); + g_assert_cmpstr (NULL, ==, str); + mu_str_free_list (lst); + g_free (str); + } + + +} + + +static void +test_mu_str_to_list (void) +{ + { + const char *items[]= {"foo", "bar ", "cuux", NULL}; + GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', FALSE); + assert_cmplst (lst, items); + mu_str_free_list (lst); + } + + { + GSList *lst = mu_str_to_list (NULL,'x',FALSE); + g_assert (lst == NULL); + mu_str_free_list (lst); + } +} + +static void +test_mu_str_to_list_strip (void) +{ + const char *items[]= {"foo", "bar", "cuux", NULL}; + GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', TRUE); + assert_cmplst (lst, items); + mu_str_free_list (lst); +} + +static void +test_mu_str_remove_ctrl_in_place (void) +{ + unsigned u; + struct { + char *str; + const char *exp; + } strings [] = { + { g_strdup(""), ""}, + { g_strdup("hello, world!"), "hello, world!" }, + { g_strdup("hello,\tworld!"), "hello, world!" }, + { g_strdup("hello,\n\nworld!"), "hello, world!", }, + { g_strdup("hello,\x1f\x1e\x1ew\nor\nld!"), "hello,w or ld!" }, + { g_strdup("\x1ehello, world!\x1f"), "hello, world!" } + }; + + for (u = 0; u != G_N_ELEMENTS(strings); ++u) { + char *res; + res = mu_str_remove_ctrl_in_place (strings[u].str); + g_assert_cmpstr (res,==,strings[u].exp); + g_free (strings[u].str); + } +} + + +int +main (int argc, char *argv[]) +{ + setlocale (LC_ALL, ""); + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/mu-str/mu-str-from-list", + test_mu_str_from_list); + g_test_add_func ("/mu-str/mu-str-to-list", + test_mu_str_to_list); + g_test_add_func ("/mu-str/mu-str-to-list-strip", + test_mu_str_to_list_strip); + + g_test_add_func ("/mu-str/mu_str_remove_ctrl_in_place", + test_mu_str_remove_ctrl_in_place); + + + return g_test_run (); +} diff --git a/lib/utils/tests/test-mu-util.c b/lib/utils/tests/test-mu-util.c new file mode 100644 index 0000000..e453ea5 --- /dev/null +++ b/lib/utils/tests/test-mu-util.c @@ -0,0 +1,286 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include + +#include +#include +#include + +#include "mu-util.h" + +static void +test_mu_util_dir_expand_00(void) +{ +#ifdef HAVE_WORDEXP_H + gchar *got, *expected; + + got = mu_util_dir_expand("~/IProbablyDoNotExist"); + expected = g_strdup_printf("%s%cIProbablyDoNotExist", + getenv("HOME"), G_DIR_SEPARATOR); + + g_assert_cmpstr(got, ==, expected); + + g_free(got); + g_free(expected); +#endif /*HAVE_WORDEXP_H*/ +} + +static void +test_mu_util_dir_expand_01(void) +{ + /* XXXX: the testcase does not work when using some dir + * setups; (see issue #585), although the code should still + * work. Turn of the test for now */ + return; + +#ifdef HAVE_WORDEXP_H + { + gchar *got, *expected; + + got = mu_util_dir_expand("~/Desktop"); + expected = g_strdup_printf("%s%cDesktop", + getenv("HOME"), G_DIR_SEPARATOR); + + g_assert_cmpstr(got, ==, expected); + + g_free(got); + g_free(expected); + } +#endif /*HAVE_WORDEXP_H*/ +} + +static void +test_mu_util_guess_maildir_01(void) +{ + char* got; + const char* expected; + + /* skip the test if there's no /tmp */ + if (access("/tmp", F_OK)) + return; + + g_setenv("MAILDIR", "/tmp", TRUE); + + got = mu_util_guess_maildir(); + expected = "/tmp"; + + g_assert_cmpstr(got, ==, expected); + g_free(got); +} + +static void +test_mu_util_guess_maildir_02(void) +{ + char *got, *mdir; + + g_unsetenv("MAILDIR"); + + mdir = g_strdup_printf("%s%cMaildir", + getenv("HOME"), G_DIR_SEPARATOR); + got = mu_util_guess_maildir(); + + if (access(mdir, F_OK) == 0) + g_assert_cmpstr(got, ==, mdir); + else + g_assert_cmpstr(got, ==, NULL); + + g_free(got); + g_free(mdir); +} + +static void +test_mu_util_check_dir_01(void) +{ + if (g_access("/usr/bin", F_OK) == 0) { + g_assert_cmpuint( + mu_util_check_dir("/usr/bin", TRUE, FALSE) == TRUE, + ==, + g_access("/usr/bin", R_OK) == 0); + } +} + +static void +test_mu_util_check_dir_02(void) +{ + if (g_access("/tmp", F_OK) == 0) { + g_assert_cmpuint( + mu_util_check_dir("/tmp", FALSE, TRUE) == TRUE, + ==, + g_access("/tmp", W_OK) == 0); + } +} + +static void +test_mu_util_check_dir_03(void) +{ + if (g_access(".", F_OK) == 0) { + g_assert_cmpuint( + mu_util_check_dir(".", TRUE, TRUE) == TRUE, + ==, + g_access(".", W_OK | R_OK) == 0); + } +} + +static void +test_mu_util_check_dir_04(void) +{ + /* not a dir, so it must be false */ + g_assert_cmpuint( + mu_util_check_dir("test-util.c", TRUE, TRUE), + ==, + FALSE); +} + +static void +test_mu_util_get_dtype_with_lstat(void) +{ + g_assert_cmpuint( + mu_util_get_dtype(MU_TESTMAILDIR, TRUE), ==, DT_DIR); + g_assert_cmpuint( + mu_util_get_dtype(MU_TESTMAILDIR2, TRUE), ==, DT_DIR); + g_assert_cmpuint( + mu_util_get_dtype(MU_TESTMAILDIR2 "/Foo/cur/mail5", TRUE), + ==, DT_REG); +} + +static void +test_mu_util_supports(void) +{ + gboolean has_guile; + gchar* path; + +#ifdef BUILD_GUILE + has_guile = TRUE; +#else + has_guile = FALSE; +#endif /*BUILD_GUILE*/ + + g_assert_cmpuint(mu_util_supports(MU_FEATURE_GUILE), ==, has_guile); + + path = g_find_program_in_path("gnuplot"); + g_free(path); + + g_assert_cmpuint(mu_util_supports(MU_FEATURE_GNUPLOT), ==, + path ? TRUE : FALSE); + + g_assert_cmpuint( + mu_util_supports(MU_FEATURE_GNUPLOT | MU_FEATURE_GUILE), + ==, + has_guile && path ? TRUE : FALSE); +} + +static void +test_mu_util_program_in_path(void) +{ + g_assert_cmpuint(mu_util_program_in_path("ls"), ==, TRUE); +} + + +static void +test_mu_util_summarize(void) +{ + const char *txt = + "Khiron was fortified and made the seat of a pargana during " + "the reign of Asaf-ud-Daula.\n\the headquarters had previously " + "been at Satanpur since its foundation and fortification by " + "the Bais raja Sathna.\n\nKhiron was also historically the seat " + "of a taluqdari estate belonging to a Janwar dynasty.\n" + "There were also several Kayasth qanungo families, " + "including many descended from Rai Sahib Rai, who had been " + "a chakladar under the Nawabs of Awadh."; + + char *summ = mu_str_summarize(txt, 3); + g_assert_cmpstr(summ, ==, + "Khiron was fortified and made the seat of a pargana " + "during the reign of Asaf-ud-Daula. he headquarters had " + "previously been at Satanpur since its foundation and " + "fortification by the Bais raja Sathna. "); + g_free (summ); +} + + +static void +test_mu_error(void) +{ + GQuark q; + GError *err; + gboolean res; + + q = mu_util_error_quark(); + g_assert_true(q != 0); + + + err = NULL; + res = mu_util_g_set_error(&err, MU_ERROR_IN_PARAMETERS, + "Hello, %s!", "World"); + + g_assert_false(res); + g_assert_cmpuint(err->domain, ==, q); + g_assert_cmpuint(err->code, ==, MU_ERROR_IN_PARAMETERS); + g_assert_cmpstr(err->message,==,"Hello, World!"); + + g_clear_error(&err); +} + + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + /* mu_util_dir_expand */ + g_test_add_func("/mu-util/mu-util-dir-expand-00", + test_mu_util_dir_expand_00); + g_test_add_func("/mu-util/mu-util-dir-expand-01", + test_mu_util_dir_expand_01); + + /* mu_util_guess_maildir */ + g_test_add_func("/mu-util/mu-util-guess-maildir-01", + test_mu_util_guess_maildir_01); + g_test_add_func("/mu-util/mu-util-guess-maildir-02", + test_mu_util_guess_maildir_02); + + /* mu_util_check_dir */ + g_test_add_func("/mu-util/mu-util-check-dir-01", + test_mu_util_check_dir_01); + g_test_add_func("/mu-util/mu-util-check-dir-02", + test_mu_util_check_dir_02); + g_test_add_func("/mu-util/mu-util-check-dir-03", + test_mu_util_check_dir_03); + g_test_add_func("/mu-util/mu-util-check-dir-04", + test_mu_util_check_dir_04); + + g_test_add_func("/mu-util/mu-util-get-dtype-with-lstat", + test_mu_util_get_dtype_with_lstat); + + g_test_add_func("/mu-util/mu-util-supports", test_mu_util_supports); + g_test_add_func("/mu-util/mu-util-program-in-path", + test_mu_util_program_in_path); + + g_test_add_func("/mu-util/summarize", test_mu_util_summarize); + g_test_add_func("/mu-util/error", test_mu_error); + + return g_test_run(); +} diff --git a/lib/utils/tests/test-option.cc b/lib/utils/tests/test-option.cc new file mode 100644 index 0000000..3313afb --- /dev/null +++ b/lib/utils/tests/test-option.cc @@ -0,0 +1,59 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include "mu-utils.hh" +#include "mu-option.hh" + +using namespace Mu; + +static Option +get_opt_int(bool b) +{ + if (b) + return Some(123); + else + return Nothing; +} + +static void +test_option() +{ + { + const auto oi{get_opt_int(true)}; + g_assert_true(!!oi); + g_assert_cmpint(oi.value(), ==, 123); + } + + { + const auto oi{get_opt_int(false)}; + g_assert_false(!!oi); + g_assert_false(oi.has_value()); + g_assert_cmpint(oi.value_or(456), ==, 456); + } +} + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/option/option", test_option); + + return g_test_run(); +} diff --git a/lib/utils/tests/test-sexp.cc b/lib/utils/tests/test-sexp.cc new file mode 100644 index 0000000..3cb1c5a --- /dev/null +++ b/lib/utils/tests/test-sexp.cc @@ -0,0 +1,190 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include +#include + +#include "mu-command-parser.hh" +#include "mu-utils.hh" +#include "mu-test-utils.hh" + +using namespace Mu; + +static bool +check_parse(const std::string& expr, const std::string& expected) +{ + try { + const auto parsed{to_string(Sexp::make_parse(expr))}; + assert_equal(parsed, expected); + return true; + + } catch (const Error& err) { + g_warning("caught exception parsing '%s': %s", expr.c_str(), err.what()); + return false; + } +} + +static void +test_parser() +{ + check_parse(":foo-123", ":foo-123"); + check_parse("foo", "foo"); + check_parse(R"(12345)", "12345"); + check_parse(R"(-12345)", "-12345"); + check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")"); + + check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\""); + + check_parse(R"("foo +bar")", + "\"foo\nbar\""); +} + +static void +test_list() +{ + const auto nstr{Sexp::make_string("foo")}; + g_assert_true(nstr.value() == "foo"); + g_assert_true(nstr.type() == Sexp::Type::String); + assert_equal(nstr.to_sexp_string(), "\"foo\""); + + const auto nnum{Sexp::make_number(123)}; + g_assert_true(nnum.value() == "123"); + g_assert_true(nnum.type() == Sexp::Type::Number); + assert_equal(nnum.to_sexp_string(), "123"); + + const auto nsym{Sexp::make_symbol("blub")}; + g_assert_true(nsym.value() == "blub"); + g_assert_true(nsym.type() == Sexp::Type::Symbol); + assert_equal(nsym.to_sexp_string(), "blub"); + + Sexp::List list; + list.add(Sexp::make_string("foo")) + .add(Sexp::make_number(123)) + .add(Sexp::make_symbol("blub")); + + const auto nlst = Sexp::make_list(std::move(list)); + g_assert_true(nlst.list().size() == 3); + g_assert_true(nlst.type() == Sexp::Type::List); + g_assert_true(nlst.list().at(1).value() == "123"); + + assert_equal(nlst.to_sexp_string(), "(\"foo\" 123 blub)"); +} + +static void +test_prop_list() +{ + Sexp::List l1; + l1.add_prop(":foo", Sexp::make_string("bar")); + Sexp s2{Sexp::make_list(std::move(l1))}; + assert_equal(s2.to_sexp_string(), "(:foo \"bar\")"); + g_assert_true(s2.is_prop_list()); + + Sexp::List l2; + const std::string x{"bar"}; + l2.add_prop(":foo", Sexp::make_string(x)); + l2.add_prop(":bar", Sexp::make_number(77)); + Sexp::List l3; + l3.add_prop(":cuux", Sexp::make_list(std::move(l2))); + Sexp s3{Sexp::make_list(std::move(l3))}; + assert_equal(s3.to_sexp_string(), "(:cuux (:foo \"bar\" :bar 77))"); +} + +static void +test_props() +{ + auto sexp2 = Sexp::make_list(Sexp::make_string("foo"), + Sexp::make_number(123), + Sexp::make_symbol("blub")); + + auto sexp = Sexp::make_prop_list(":foo", + Sexp::make_string("bär"), + ":cuux", + Sexp::make_number(123), + ":flub", + Sexp::make_symbol("fnord"), + ":boo", + std::move(sexp2)); + + assert_equal(sexp.to_sexp_string(), + "(:foo \"b\303\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))"); +} + +static void +test_prop_list_remove() +{ + { + Sexp::List lst; + lst.add_prop(":foo", Sexp::make_string("123")) + .add_prop(":bar", Sexp::make_number(123)); + + assert_equal(Sexp::make_list(std::move(lst)).to_sexp_string(), + R"((:foo "123" :bar 123))"); + } + + { + Sexp::List lst; + lst.add_prop(":foo", Sexp::make_string("123")) + .add_prop(":bar", Sexp::make_number(123)); + + assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(), + R"((:foo "123" :bar 123))"); + + lst.remove_prop(":bar"); + + assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(), + R"((:foo "123"))"); + + lst.clear(); + g_assert_cmpuint(lst.size(), ==, 0); + } + + { + Sexp::List lst; + lst.add(Sexp::make_number(123)); + Sexp s2{Sexp::make_list(std::move(lst))}; + g_assert_false(s2.is_prop_list()); + } +} + +int +main(int argc, char* argv[]) +try { + mu_test_init(&argc, &argv); + + if (argc == 2) { + std::cout << Sexp::make_parse(argv[1]) << '\n'; + return 0; + } + + g_test_add_func("/utils/sexp/parser", test_parser); + g_test_add_func("/utils/sexp/list", test_list); + g_test_add_func("/utils/sexp/proplist", test_prop_list); + g_test_add_func("/utils/sexp/proplist-remove", test_prop_list_remove); + g_test_add_func("/utils/sexp/props", test_props); + + return g_test_run(); + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} diff --git a/lib/utils/tests/test-utils.cc b/lib/utils/tests/test-utils.cc new file mode 100644 index 0000000..b69c705 --- /dev/null +++ b/lib/utils/tests/test-utils.cc @@ -0,0 +1,322 @@ +/* +** Copyright (C) 2017-2022 Dirk-Jan C. Binnema +** +** 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 library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include +#include +#include +#include + +#include "mu-utils.hh" +#include "mu-test-utils.hh" +#include "mu-error.hh" + +using namespace Mu; + + +struct Case { + const std::string expr; + bool is_first{}; + const std::string expected; +}; +using CaseVec = std::vector; +using ProcFunc = std::function; + +static void +test_cases(const CaseVec& cases, ProcFunc proc) +{ + for (const auto& casus : cases) { + const auto res = proc(casus.expr, casus.is_first); + if (g_test_verbose()) { + std::cout << "\n"; + std::cout << casus.expr << ' ' << casus.is_first << std::endl; + std::cout << "exp: '" << casus.expected << "'" << std::endl; + std::cout << "got: '" << res << "'" << std::endl; + } + + g_assert_true(casus.expected == res); + } +} + +static void +test_date_basic() +{ + const auto hki = "Europe/Helsinki"; + + // ensure we have the needed TZ or skip the test. + if (!timezone_available(hki)) { + g_test_skip("timezone Europe/Helsinki not available"); + return; + } + + g_setenv("TZ", hki, TRUE); + constexpr std::array, 13> cases = {{ + {"2015-09-18T09:10:23", true, 1442556623}, + {"1972-12-14T09:10:23", true, 93165023}, + {"1854-11-18T17:10:23", true, 0}, + + {"2000-02-31T09:10:23", true, 951861599}, + {"2000-02-29T23:59:59", true, 951861599}, + + {"20220602", true, 1654117200}, + {"20220605", false, 1654462799}, + + {"202206", true, 1654030800}, + {"202206", false, 1656622799}, + + {"2016", true, 1451599200}, + {"2016", false, 1483221599}, + + // {"fnorb", true, -1}, + // {"fnorb", false, -1}, + {"", false, G_MAXINT64}, + {"", true, 0} + }}; + + for (auto& test: cases) { + if (g_test_verbose()) + g_debug("checking %s", std::get<0>(test)); + g_assert_cmpuint(parse_date_time(std::get<0>(test), + std::get<1>(test)).value_or(-1),==, + std::get<2>(test)); + } +} + +static void +test_date_ymwdhMs(void) +{ + struct testcase { + std::string expr; + int64_t diff; + int tolerance; + }; + + std::array cases = {{ + {"7s", 7, 1}, + {"3M", 3 * 60, 1}, + {"3h", 3 * 60 * 60, 1}, + {"21d", 21 * 24 * 60 * 60, 3600 + 1}, + {"2w", 2 * 7 * 24 * 60 * 60, 3600 + 1}, + {"2y", 2 * 365 * 24 * 60 * 60, 24 * 3600 + 1}, + {"3m", 3 * 30 * 24 * 60 * 60, 3 * 24 * 3600 + 1} + }}; + + for (auto&& tcase: cases) { + const auto date = parse_date_time(tcase.expr, true); + g_assert_true(date); + const auto diff = ::time({}) - *date; + if (g_test_verbose()) + std::cerr << tcase.expr << ' ' << diff << ' ' << tcase.diff << '\n'; + + g_assert_true(tcase.diff - diff <= tcase.tolerance); + } + + // note: perhaps it'd be nice if we'd detect this error; + // currently we're being rather tolerant + // g_assert_false(!!parse_date_time("25q", false)); +} + +static void +test_parse_size() +{ + constexpr std::array, 6> cases = {{ + { "456", false, 456 }, + { "", false, G_MAXINT64 }, + { "", true, 0 }, + { "2K", false, 2048 }, + { "2M", true, 2097152 }, + { "5G", true, 5368709120 } + }}; + for(auto&& test: cases) { + g_assert_cmpint(parse_size(std::get<0>(test), std::get<1>(test)) + .value_or(-1), ==, std::get<2>(test)); + } + + g_assert_false(!!parse_size("-1", true)); + g_assert_false(!!parse_size("scoobydoobydoo", false)); +} + +static void +test_flatten() +{ + CaseVec cases = { + {"Менделе́ев", true, "менделеев"}, + {"", false, ""}, + {"Ångström", true, "angstrom"}, + }; + + test_cases(cases, [](auto s, auto f) { return utf8_flatten(s); }); +} + +static void +test_remove_ctrl() +{ + CaseVec cases = { + {"Foo\n\nbar", true, "Foo bar"}, + {"", false, ""}, + {" ", false, " "}, + {"Hello World ", false, "Hello World "}, + {"Ångström", false, "Ångström"}, + }; + + test_cases(cases, [](auto s, auto f) { return remove_ctrl(s); }); +} + +static void +test_clean() +{ + CaseVec cases = { + {"\t a\t\nb ", true, "a b"}, + {"", false, ""}, + {"Ångström", true, "Ångström"}, + }; + + test_cases(cases, [](auto s, auto f) { return utf8_clean(s); }); +} + +static void +test_format() +{ + g_assert_true(format("hello %s", "world") == "hello world"); + g_assert_true(format("hello %s, %u", "world", 123) == "hello world, 123"); +} + +static void +test_split() +{ + using svec = std::vector; + auto assert_equal_svec=[](const svec& sv1, const svec& sv2) { + g_assert_cmpuint(sv1.size(),==,sv2.size()); + for (auto i = 0U; i != sv1.size(); ++i) + g_assert_cmpstr(sv1[i].c_str(),==,sv2[i].c_str()); + }; + + // string sepa + assert_equal_svec(split("axbxc", "x"), {"a", "b", "c"}); + assert_equal_svec(split("axbxcx", "x"), {"a", "b", "c", ""}); + assert_equal_svec(split("", "boo"), {}); + assert_equal_svec(split("ayybyyc", "yy"), {"a", "b", "c"}); + assert_equal_svec(split("abc", ""), {"a", "b", "c"}); + assert_equal_svec(split("", "boo"), {}); + + // char sepa + assert_equal_svec(split("axbxc", 'x'), {"a", "b", "c"}); + assert_equal_svec(split("axbxcx", 'x'), {"a", "b", "c", ""}); + + // rx sexp + assert_equal_svec(split("axbyc", std::regex("[xy]")), {"a", "b", "c"}); +} + +static void +test_join() +{ + assert_equal(join({"a", "b", "c"}, "x"), "axbxc"); + assert_equal(join({"a", "b", "c"}, ""), "abc"); + assert_equal(join({},"foo"), ""); + assert_equal(join({"d", "e", "f"}, "foo"), "dfooefoof"); +} + + +enum struct Bits { None = 0, Bit1 = 1 << 0, Bit2 = 1 << 1 }; +MU_ENABLE_BITOPS(Bits); + +static void +test_define_bitmap() +{ + g_assert_cmpuint((guint)Bits::None, ==, (guint)0); + g_assert_cmpuint((guint)Bits::Bit1, ==, (guint)1); + g_assert_cmpuint((guint)Bits::Bit2, ==, (guint)2); + + g_assert_cmpuint((guint)(Bits::Bit1 | Bits::Bit2), ==, (guint)3); + g_assert_cmpuint((guint)(Bits::Bit1 & Bits::Bit2), ==, (guint)0); + + g_assert_cmpuint((guint)(Bits::Bit1 & (~Bits::Bit2)), ==, (guint)1); + + { + Bits b{Bits::Bit1}; + b |= Bits::Bit2; + g_assert_cmpuint((guint)b, ==, (guint)3); + } + + { + Bits b{Bits::Bit1}; + b &= Bits::Bit1; + g_assert_cmpuint((guint)b, ==, (guint)1); + } +} + +static void +test_to_from_lexnum() +{ + assert_equal(to_lexnum(0), "g0"); + assert_equal(to_lexnum(100), "h64"); + assert_equal(to_lexnum(12345), "j3039"); + + g_assert_cmpuint(from_lexnum(to_lexnum(0)), ==, 0); + g_assert_cmpuint(from_lexnum(to_lexnum(7777)), ==, 7777); + g_assert_cmpuint(from_lexnum(to_lexnum(9876543)), ==, 9876543); +} + +static void +test_locale_workaround() +{ + g_assert_true(locale_workaround()); + + g_setenv("LC_ALL", "BOO", 1); + + g_assert_true(locale_workaround()); +} + +static void +test_error() +{ + GError *err; + err = g_error_new(MU_ERROR_DOMAIN, 77, "Hello, %s", "world"); + Error ex{Error::Code::Crypto, &err, "boo"}; + g_assert_cmpstr(ex.what(), ==, "boo: Hello, world"); + + ex.fill_g_error(&err); + g_assert_cmpuint(err->code, ==, static_cast(Error::Code::Crypto)); + g_clear_error(&err); +} + + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_add_func("/utils/date-basic", test_date_basic); + g_test_add_func("/utils/date-ymwdhMs", test_date_ymwdhMs); + g_test_add_func("/utils/parse-size", test_parse_size); + g_test_add_func("/utils/flatten", test_flatten); + g_test_add_func("/utils/remove-ctrl", test_remove_ctrl); + g_test_add_func("/utils/clean", test_clean); + g_test_add_func("/utils/format", test_format); + g_test_add_func("/utils/split", test_split); + g_test_add_func("/utils/join", test_join); + g_test_add_func("/utils/define-bitmap", test_define_bitmap); + g_test_add_func("/utils/to-from-lexnum", test_to_from_lexnum); + g_test_add_func("/utils/locale-workaround", test_locale_workaround); + g_test_add_func("/utils/error", test_error); + + return g_test_run(); +} diff --git a/m4/Makefile.am b/m4/Makefile.am new file mode 100644 index 0000000..c6a97e3 --- /dev/null +++ b/m4/Makefile.am @@ -0,0 +1,47 @@ +## Copyright (C) 2008-2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +EXTRA_DIST= \ + ax_ac_append_to_file.m4 \ + ax_ac_print_to_file.m4 \ + ax_add_am_macro_static.m4 \ + ax_am_macros_static.m4 \ + ax_append_compile_flags.m4 \ + ax_append_flag.m4 \ + ax_append_link_flags.m4 \ + ax_check_compile_flag.m4 \ + ax_check_enable_debug.m4 \ + ax_check_gnu_make.m4 \ + ax_check_link_flag.m4 \ + ax_code_coverage.m4 \ + ax_compiler_flags.m4 \ + ax_compiler_flags_cflags.m4 \ + ax_compiler_flags_cxxflags.m4 \ + ax_compiler_flags_gir.m4 \ + ax_compiler_flags_ldflags.m4 \ + ax_cxx_compile_stdcxx.m4 \ + ax_cxx_compile_stdcxx_17.m4 \ + ax_file_escapes.m4 \ + ax_is_release.m4 \ + ax_lib_readline.m4 \ + ax_require_defined.m4 \ + ax_valgrind_check.m4 \ + guile.m4 \ + lib-ld.m4 \ + lib-link.m4 \ + lib-prefix.m4 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 +# +# 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 +# +# 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 +# Copyright (c) 2009 Allan Caffee +# +# 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 +# Copyright (c) 2009 Allan Caffee +# +# 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_append_compile_flags.m4 b/m4/ax_append_compile_flags.m4 new file mode 100644 index 0000000..9c85635 --- /dev/null +++ b/m4/ax_append_compile_flags.m4 @@ -0,0 +1,46 @@ +# ============================================================================ +# https://www.gnu.org/software/autoconf-archive/ax_append_compile_flags.html +# ============================================================================ +# +# SYNOPSIS +# +# AX_APPEND_COMPILE_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT]) +# +# DESCRIPTION +# +# For every FLAG1, FLAG2 it is checked whether the compiler works with the +# flag. If it does, the flag is added FLAGS-VARIABLE +# +# If FLAGS-VARIABLE is not specified, the current language's flags (e.g. +# CFLAGS) is used. During the check the flag is always added to the +# current language's flags. +# +# If EXTRA-FLAGS is defined, it is added to the current language's default +# flags (e.g. CFLAGS) when the check is done. The check is thus made with +# the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to +# force the compiler to issue an error when a bad flag is given. +# +# INPUT gives an alternative input source to AC_COMPILE_IFELSE. +# +# NOTE: This macro depends on the AX_APPEND_FLAG and +# AX_CHECK_COMPILE_FLAG. Please keep this macro in sync with +# AX_APPEND_LINK_FLAGS. +# +# LICENSE +# +# Copyright (c) 2011 Maarten Bosmans +# +# 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 7 + +AC_DEFUN([AX_APPEND_COMPILE_FLAGS], +[AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) +AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) +for flag in $1; do + AX_CHECK_COMPILE_FLAG([$flag], [AX_APPEND_FLAG([$flag], [$2])], [], [$3], [$4]) +done +])dnl AX_APPEND_COMPILE_FLAGS diff --git a/m4/ax_append_flag.m4 b/m4/ax_append_flag.m4 new file mode 100644 index 0000000..dd6d8b6 --- /dev/null +++ b/m4/ax_append_flag.m4 @@ -0,0 +1,50 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_append_flag.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_APPEND_FLAG(FLAG, [FLAGS-VARIABLE]) +# +# DESCRIPTION +# +# FLAG is appended to the FLAGS-VARIABLE shell variable, with a space +# added in between. +# +# If FLAGS-VARIABLE is not specified, the current language's flags (e.g. +# CFLAGS) is used. FLAGS-VARIABLE is not changed if it already contains +# FLAG. If FLAGS-VARIABLE is unset in the shell, it is set to exactly +# FLAG. +# +# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. +# +# LICENSE +# +# Copyright (c) 2008 Guido U. Draheim +# Copyright (c) 2011 Maarten Bosmans +# +# 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_APPEND_FLAG], +[dnl +AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_SET_IF +AS_VAR_PUSHDEF([FLAGS], [m4_default($2,_AC_LANG_PREFIX[FLAGS])]) +AS_VAR_SET_IF(FLAGS,[ + AS_CASE([" AS_VAR_GET(FLAGS) "], + [*" $1 "*], [AC_RUN_LOG([: FLAGS already contains $1])], + [ + AS_VAR_APPEND(FLAGS,[" $1"]) + AC_RUN_LOG([: FLAGS="$FLAGS"]) + ]) + ], + [ + AS_VAR_SET(FLAGS,[$1]) + AC_RUN_LOG([: FLAGS="$FLAGS"]) + ]) +AS_VAR_POPDEF([FLAGS])dnl +])dnl AX_APPEND_FLAG diff --git a/m4/ax_append_link_flags.m4 b/m4/ax_append_link_flags.m4 new file mode 100644 index 0000000..99b9fa5 --- /dev/null +++ b/m4/ax_append_link_flags.m4 @@ -0,0 +1,44 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_append_link_flags.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_APPEND_LINK_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT]) +# +# DESCRIPTION +# +# For every FLAG1, FLAG2 it is checked whether the linker works with the +# flag. If it does, the flag is added FLAGS-VARIABLE +# +# If FLAGS-VARIABLE is not specified, the linker's flags (LDFLAGS) is +# used. During the check the flag is always added to the linker's flags. +# +# 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_COMPILE_IFELSE. +# +# NOTE: This macro depends on the AX_APPEND_FLAG and AX_CHECK_LINK_FLAG. +# Please keep this macro in sync with AX_APPEND_COMPILE_FLAGS. +# +# LICENSE +# +# Copyright (c) 2011 Maarten Bosmans +# +# 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 7 + +AC_DEFUN([AX_APPEND_LINK_FLAGS], +[AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG]) +AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) +for flag in $1; do + AX_CHECK_LINK_FLAG([$flag], [AX_APPEND_FLAG([$flag], [m4_default([$2], [LDFLAGS])])], [], [$3], [$4]) +done +])dnl AX_APPEND_LINK_FLAGS diff --git a/m4/ax_check_compile_flag.m4 b/m4/ax_check_compile_flag.m4 new file mode 100644 index 0000000..bd753b3 --- /dev/null +++ b/m4/ax_check_compile_flag.m4 @@ -0,0 +1,53 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_compile_flag.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_COMPILE_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT]) +# +# DESCRIPTION +# +# Check whether the given FLAG works with the current language's compiler +# 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 current language's default +# flags (e.g. CFLAGS) when the check is done. The check is thus made with +# the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to +# force the compiler to issue an error when a bad flag is given. +# +# INPUT gives an alternative input source to AC_COMPILE_IFELSE. +# +# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this +# macro in sync with AX_CHECK_{PREPROC,LINK}_FLAG. +# +# LICENSE +# +# Copyright (c) 2008 Guido U. Draheim +# Copyright (c) 2011 Maarten Bosmans +# +# 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 6 + +AC_DEFUN([AX_CHECK_COMPILE_FLAG], +[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF +AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl +AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [ + ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS + _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1" + AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])], + [AS_VAR_SET(CACHEVAR,[yes])], + [AS_VAR_SET(CACHEVAR,[no])]) + _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags]) +AS_VAR_IF(CACHEVAR,yes, + [m4_default([$2], :)], + [m4_default([$3], :)]) +AS_VAR_POPDEF([CACHEVAR])dnl +])dnl AX_CHECK_COMPILE_FLAGS diff --git a/m4/ax_check_enable_debug.m4 b/m4/ax_check_enable_debug.m4 new file mode 100644 index 0000000..7bc7710 --- /dev/null +++ b/m4/ax_check_enable_debug.m4 @@ -0,0 +1,124 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_enable_debug.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_ENABLE_DEBUG([enable by default=yes/info/profile/no], [ENABLE DEBUG VARIABLES ...], [DISABLE DEBUG VARIABLES NDEBUG ...], [IS-RELEASE]) +# +# DESCRIPTION +# +# Check for the presence of an --enable-debug option to configure, with +# the specified default value used when the option is not present. Return +# the value in the variable $ax_enable_debug. +# +# Specifying 'yes' adds '-g -O0' to the compilation flags for all +# languages. Specifying 'info' adds '-g' to the compilation flags. +# Specifying 'profile' adds '-g -pg' to the compilation flags and '-pg' to +# the linking flags. Otherwise, nothing is added. +# +# Define the variables listed in the second argument if debug is enabled, +# defaulting to no variables. Defines the variables listed in the third +# argument if debug is disabled, defaulting to NDEBUG. All lists of +# variables should be space-separated. +# +# If debug is not enabled, ensure AC_PROG_* will not add debugging flags. +# Should be invoked prior to any AC_PROG_* compiler checks. +# +# IS-RELEASE can be used to change the default to 'no' when making a +# release. Set IS-RELEASE to 'yes' or 'no' as appropriate. By default, it +# uses the value of $ax_is_release, so if you are using the AX_IS_RELEASE +# macro, there is no need to pass this parameter. +# +# AX_IS_RELEASE([git-directory]) +# AX_CHECK_ENABLE_DEBUG() +# +# LICENSE +# +# Copyright (c) 2011 Rhys Ulerich +# Copyright (c) 2014, 2015 Philip Withnall +# +# 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. + +#serial 9 + +AC_DEFUN([AX_CHECK_ENABLE_DEBUG],[ + AC_BEFORE([$0],[AC_PROG_CC])dnl + AC_BEFORE([$0],[AC_PROG_CXX])dnl + AC_BEFORE([$0],[AC_PROG_F77])dnl + AC_BEFORE([$0],[AC_PROG_FC])dnl + + AC_MSG_CHECKING(whether to enable debugging) + + ax_enable_debug_default=m4_tolower(m4_normalize(ifelse([$1],,[no],[$1]))) + ax_enable_debug_is_release=m4_tolower(m4_normalize(ifelse([$4],, + [$ax_is_release], + [$4]))) + + # If this is a release, override the default. + AS_IF([test "$ax_enable_debug_is_release" = "yes"], + [ax_enable_debug_default="no"]) + + m4_define(ax_enable_debug_vars,[m4_normalize(ifelse([$2],,,[$2]))]) + m4_define(ax_disable_debug_vars,[m4_normalize(ifelse([$3],,[NDEBUG],[$3]))]) + + AC_ARG_ENABLE(debug, + [AS_HELP_STRING([--enable-debug=]@<:@yes/info/profile/no@:>@,[compile with debugging])], + [],enable_debug=$ax_enable_debug_default) + + # empty mean debug yes + AS_IF([test "x$enable_debug" = "x"], + [enable_debug="yes"]) + + # case of debug + AS_CASE([$enable_debug], + [yes],[ + AC_MSG_RESULT(yes) + CFLAGS="${CFLAGS} -g -O0" + CXXFLAGS="${CXXFLAGS} -g -O0" + FFLAGS="${FFLAGS} -g -O0" + FCFLAGS="${FCFLAGS} -g -O0" + OBJCFLAGS="${OBJCFLAGS} -g -O0" + ], + [info],[ + AC_MSG_RESULT(info) + CFLAGS="${CFLAGS} -g" + CXXFLAGS="${CXXFLAGS} -g" + FFLAGS="${FFLAGS} -g" + FCFLAGS="${FCFLAGS} -g" + OBJCFLAGS="${OBJCFLAGS} -g" + ], + [profile],[ + AC_MSG_RESULT(profile) + CFLAGS="${CFLAGS} -g -pg" + CXXFLAGS="${CXXFLAGS} -g -pg" + FFLAGS="${FFLAGS} -g -pg" + FCFLAGS="${FCFLAGS} -g -pg" + OBJCFLAGS="${OBJCFLAGS} -g -pg" + LDFLAGS="${LDFLAGS} -pg" + ], + [ + AC_MSG_RESULT(no) + dnl Ensure AC_PROG_CC/CXX/F77/FC/OBJC will not enable debug flags + dnl by setting any unset environment flag variables + AS_IF([test "x${CFLAGS+set}" != "xset"], + [CFLAGS=""]) + AS_IF([test "x${CXXFLAGS+set}" != "xset"], + [CXXFLAGS=""]) + AS_IF([test "x${FFLAGS+set}" != "xset"], + [FFLAGS=""]) + AS_IF([test "x${FCFLAGS+set}" != "xset"], + [FCFLAGS=""]) + AS_IF([test "x${OBJCFLAGS+set}" != "xset"], + [OBJCFLAGS=""]) + ]) + + dnl Define various variables if debugging is disabled. + dnl assert.h is a NOP if NDEBUG is defined, so define it by default. + AS_IF([test "x$enable_debug" = "xyes"], + [m4_map_args_w(ax_enable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is enabled])])], + [m4_map_args_w(ax_disable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is disabled])])]) + ax_enable_debug=$enable_debug +]) diff --git a/m4/ax_check_gnu_make.m4 b/m4/ax_check_gnu_make.m4 new file mode 100644 index 0000000..6811043 --- /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 +# Copyright (c) 2015 Enrico M. Crisostomo +# +# 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([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([ifGNUmake], ["#"])]) + 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..03a30ce --- /dev/null +++ b/m4/ax_check_link_flag.m4 @@ -0,0 +1,53 @@ +# =========================================================================== +# 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 +# Copyright (c) 2011 Maarten Bosmans +# +# 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 6 + +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_code_coverage.m4 b/m4/ax_code_coverage.m4 new file mode 100644 index 0000000..6d08319 --- /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 . + +#serial 33 + +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" + CODE_COVERAGE_CXXFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" + 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_compiler_flags.m4 b/m4/ax_compiler_flags.m4 new file mode 100644 index 0000000..ddb0456 --- /dev/null +++ b/m4/ax_compiler_flags.m4 @@ -0,0 +1,158 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS([CFLAGS-VARIABLE], [LDFLAGS-VARIABLE], [IS-RELEASE], [EXTRA-BASE-CFLAGS], [EXTRA-YES-CFLAGS], [UNUSED], [UNUSED], [UNUSED], [EXTRA-BASE-LDFLAGS], [EXTRA-YES-LDFLAGS], [UNUSED], [UNUSED], [UNUSED]) +# +# DESCRIPTION +# +# Check for the presence of an --enable-compile-warnings option to +# configure, defaulting to "error" in normal operation, or "yes" if +# IS-RELEASE is equal to "yes". Return the value in the variable +# $ax_enable_compile_warnings. +# +# Depending on the value of --enable-compile-warnings, different compiler +# warnings are checked to see if they work with the current compiler and, +# if so, are appended to CFLAGS-VARIABLE and LDFLAGS-VARIABLE. This +# allows a consistent set of baseline compiler warnings to be used across +# a code base, irrespective of any warnings enabled locally by individual +# developers. By standardising the warnings used by all developers of a +# project, the project can commit to a zero-warnings policy, using -Werror +# to prevent compilation if new warnings are introduced. This makes +# catching bugs which are flagged by warnings a lot easier. +# +# By providing a consistent --enable-compile-warnings argument across all +# projects using this macro, continuous integration systems can easily be +# configured the same for all projects. Automated systems or build +# systems aimed at beginners may want to pass the --disable-Werror +# argument to unconditionally prevent warnings being fatal. +# +# --enable-compile-warnings can take the values: +# +# * no: Base compiler warnings only; not even -Wall. +# * yes: The above, plus a broad range of useful warnings. +# * error: The above, plus -Werror so that all warnings are fatal. +# Use --disable-Werror to override this and disable fatal +# warnings. +# +# The set of base and enabled flags can be augmented using the +# EXTRA-*-CFLAGS and EXTRA-*-LDFLAGS variables, which are tested and +# appended to the output variable if --enable-compile-warnings is not +# "no". Flags should not be disabled using these arguments, as the entire +# point of AX_COMPILER_FLAGS is to enforce a consistent set of useful +# compiler warnings on code, using warnings which have been chosen for low +# false positive rates. If a compiler emits false positives for a +# warning, a #pragma should be used in the code to disable the warning +# locally. See: +# +# https://gcc.gnu.org/onlinedocs/gcc-4.9.2/gcc/Diagnostic-Pragmas.html#Diagnostic-Pragmas +# +# The EXTRA-* variables should only be used to supply extra warning flags, +# and not general purpose compiler flags, as they are controlled by +# configure options such as --disable-Werror. +# +# IS-RELEASE can be used to disable -Werror when making a release, which +# is useful for those hairy moments when you just want to get the release +# done as quickly as possible. Set it to "yes" to disable -Werror. By +# default, it uses the value of $ax_is_release, so if you are using the +# AX_IS_RELEASE macro, there is no need to pass this parameter. For +# example: +# +# AX_IS_RELEASE([git-directory]) +# AX_COMPILER_FLAGS() +# +# CFLAGS-VARIABLE defaults to WARN_CFLAGS, and LDFLAGS-VARIABLE defaults +# to WARN_LDFLAGS. Both variables are AC_SUBST-ed by this macro, but must +# be manually added to the CFLAGS and LDFLAGS variables for each target in +# the code base. +# +# If C++ language support is enabled with AC_PROG_CXX, which must occur +# before this macro in configure.ac, warning flags for the C++ compiler +# are AC_SUBST-ed as WARN_CXXFLAGS, and must be manually added to the +# CXXFLAGS variables for each target in the code base. EXTRA-*-CFLAGS can +# be used to augment the base and enabled flags. +# +# Warning flags for g-ir-scanner (from GObject Introspection) are +# AC_SUBST-ed as WARN_SCANNERFLAGS. This variable must be manually added +# to the SCANNERFLAGS variable for each GIR target in the code base. If +# extra g-ir-scanner flags need to be enabled, the AX_COMPILER_FLAGS_GIR +# macro must be invoked manually. +# +# AX_COMPILER_FLAGS may add support for other tools in future, in addition +# to the compiler and linker. No extra EXTRA-* variables will be added +# for those tools, and all extra support will still use the single +# --enable-compile-warnings configure option. For finer grained control +# over the flags for individual tools, use AX_COMPILER_FLAGS_CFLAGS, +# AX_COMPILER_FLAGS_LDFLAGS and AX_COMPILER_FLAGS_* for new tools. +# +# The UNUSED variables date from a previous version of this macro, and are +# automatically appended to the preceding non-UNUSED variable. They should +# be left empty in new uses of the macro. +# +# LICENSE +# +# Copyright (c) 2014, 2015 Philip Withnall +# Copyright (c) 2015 David King +# +# 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 14 + +# _AX_COMPILER_FLAGS_LANG([LANGNAME]) +m4_defun([_AX_COMPILER_FLAGS_LANG], +[m4_ifdef([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [], + [m4_define([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [])dnl + AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_]$1[FLAGS])])dnl +]) + +AC_DEFUN([AX_COMPILER_FLAGS],[ + # C support is enabled by default. + _AX_COMPILER_FLAGS_LANG([C]) + # Only enable C++ support if AC_PROG_CXX is called. The redefinition of + # AC_PROG_CXX is so that a fatal error is emitted if this macro is called + # before AC_PROG_CXX, which would otherwise cause no C++ warnings to be + # checked. + AC_PROVIDE_IFELSE([AC_PROG_CXX], + [_AX_COMPILER_FLAGS_LANG([CXX])], + [m4_define([AC_PROG_CXX], defn([AC_PROG_CXX])[_AX_COMPILER_FLAGS_LANG([CXX])])]) + AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_LDFLAGS]) + + # Default value for IS-RELEASE is $ax_is_release + ax_compiler_flags_is_release=m4_tolower(m4_normalize(ifelse([$3],, + [$ax_is_release], + [$3]))) + + AC_ARG_ENABLE([compile-warnings], + AS_HELP_STRING([--enable-compile-warnings=@<:@no/yes/error@:>@], + [Enable compiler warnings and errors]),, + [AS_IF([test "$ax_compiler_flags_is_release" = "yes"], + [enable_compile_warnings="yes"], + [enable_compile_warnings="error"])]) + AC_ARG_ENABLE([Werror], + AS_HELP_STRING([--disable-Werror], + [Unconditionally make all compiler warnings non-fatal]),, + [enable_Werror=maybe]) + + # Return the user's chosen warning level + AS_IF([test "$enable_Werror" = "no" -a \ + "$enable_compile_warnings" = "error"],[ + enable_compile_warnings="yes" + ]) + + ax_enable_compile_warnings=$enable_compile_warnings + + AX_COMPILER_FLAGS_CFLAGS([$1],[$ax_compiler_flags_is_release], + [$4],[$5 $6 $7 $8]) + m4_ifdef([_AX_COMPILER_FLAGS_LANG_CXX_enabled], + [AX_COMPILER_FLAGS_CXXFLAGS([WARN_CXXFLAGS], + [$ax_compiler_flags_is_release], + [$4],[$5 $6 $7 $8])]) + AX_COMPILER_FLAGS_LDFLAGS([$2],[$ax_compiler_flags_is_release], + [$9],[$10 $11 $12 $13]) + AX_COMPILER_FLAGS_GIR([WARN_SCANNERFLAGS],[$ax_compiler_flags_is_release]) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_compiler_flags_cflags.m4 b/m4/ax_compiler_flags_cflags.m4 new file mode 100644 index 0000000..916f918 --- /dev/null +++ b/m4/ax_compiler_flags_cflags.m4 @@ -0,0 +1,161 @@ +# ============================================================================= +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cflags.html +# ============================================================================= +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_CFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the C compiler to VARIABLE, which defaults to +# WARN_CFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be +# manually added to the CFLAGS variable for each target in the code base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2014, 2015 Philip Withnall +# Copyright (c) 2017, 2018 Reini Urban +# +# 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 + +AC_DEFUN([AX_COMPILER_FLAGS_CFLAGS],[ + AC_REQUIRE([AC_PROG_SED]) + AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS]) + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) + + # Variable names + m4_define([ax_warn_cflags_variable], + [m4_normalize(ifelse([$1],,[WARN_CFLAGS],[$1]))]) + + AC_LANG_PUSH([C]) + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + [#ifndef __cplusplus + #error "no C++" + #endif]])], + [ax_compiler_cxx=yes;], + [ax_compiler_cxx=no;]) + + # Always pass -Werror=unknown-warning-option to get Clang to fail on bad + # flags, otherwise they are always appended to the warn_cflags variable, and + # Clang warns on them for every compilation unit. + # If this is passed to GCC, it will explode, so the flag must be enabled + # conditionally. + AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[ + ax_compiler_flags_test="-Werror=unknown-warning-option" + ],[ + ax_compiler_flags_test="" + ]) + + # Check that -Wno-suggest-attribute=format is supported + AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[ + ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format" + ],[ + ax_compiler_no_suggest_attribute_flags="" + ]) + + # Base flags + AX_APPEND_COMPILE_FLAGS([ dnl + -fno-strict-aliasing dnl + $3 dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + if test "$ax_compiler_cxx" = "no" ; then + # C-only flags. Warn in C++ + AX_APPEND_COMPILE_FLAGS([ dnl + -Wnested-externs dnl + -Wmissing-prototypes dnl + -Wstrict-prototypes dnl + -Wdeclaration-after-statement dnl + -Wimplicit-function-declaration dnl + -Wold-style-definition dnl + -Wjump-misses-init dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + fi + + # "yes" flags + AX_APPEND_COMPILE_FLAGS([ dnl + -Wall dnl + -Wextra dnl + -Wundef dnl + -Wwrite-strings dnl + -Wpointer-arith dnl + -Wmissing-declarations dnl + -Wredundant-decls dnl + -Wno-unused-parameter dnl + -Wno-missing-field-initializers dnl + -Wformat=2 dnl + -Wcast-align dnl + -Wformat-nonliteral dnl + -Wformat-security dnl + -Wsign-compare dnl + -Wstrict-aliasing dnl + -Wshadow dnl + -Winline dnl + -Wpacked dnl + -Wmissing-format-attribute dnl + -Wmissing-noreturn dnl + -Winit-self dnl + -Wredundant-decls dnl + -Wmissing-include-dirs dnl + -Wunused-but-set-variable dnl + -Warray-bounds dnl + -Wreturn-type dnl + -Wswitch-enum dnl + -Wswitch-default dnl + -Wduplicated-cond dnl + -Wduplicated-branches dnl + -Wlogical-op dnl + -Wrestrict dnl + -Wnull-dereference dnl + -Wdouble-promotion dnl + $4 dnl + $5 dnl + $6 dnl + $7 dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags; -Werror has to be appended unconditionally because + # it's not possible to test for + # + # suggest-attribute=format is disabled because it gives too many false + # positives + AX_APPEND_FLAG([-Werror],ax_warn_cflags_variable) + + AX_APPEND_COMPILE_FLAGS([ dnl + [$ax_compiler_no_suggest_attribute_flags] dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + ]) + + # In the flags below, when disabling specific flags, always add *both* + # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example) + # we enable -Werror, disable a flag, and a build bot passes CFLAGS=-Wall, + # which effectively turns that flag back on again as an error. + for flag in $ax_warn_cflags_variable; do + AS_CASE([$flag], + [-Wno-*=*],[], + [-Wno-*],[ + AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')], + ax_warn_cflags_variable, + [$ax_compiler_flags_test]) + ]) + done + + AC_LANG_POP([C]) + + # Substitute the variables + AC_SUBST(ax_warn_cflags_variable) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_compiler_flags_cxxflags.m4 b/m4/ax_compiler_flags_cxxflags.m4 new file mode 100644 index 0000000..3067d9b --- /dev/null +++ b/m4/ax_compiler_flags_cxxflags.m4 @@ -0,0 +1,136 @@ +# =============================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cxxflags.html +# =============================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_CXXFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the C++ compiler to VARIABLE, which defaults to +# WARN_CXXFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be +# manually added to the CXXFLAGS variable for each target in the code +# base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2015 David King +# +# 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_COMPILER_FLAGS_CXXFLAGS],[ + AC_REQUIRE([AC_PROG_SED]) + AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS]) + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) + + # Variable names + m4_define([ax_warn_cxxflags_variable], + [m4_normalize(ifelse([$1],,[WARN_CXXFLAGS],[$1]))]) + + AC_LANG_PUSH([C++]) + + # Always pass -Werror=unknown-warning-option to get Clang to fail on bad + # flags, otherwise they are always appended to the warn_cxxflags variable, + # and Clang warns on them for every compilation unit. + # If this is passed to GCC, it will explode, so the flag must be enabled + # conditionally. + AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[ + ax_compiler_flags_test="-Werror=unknown-warning-option" + ],[ + ax_compiler_flags_test="" + ]) + + # Check that -Wno-suggest-attribute=format is supported + AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[ + ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format" + ],[ + ax_compiler_no_suggest_attribute_flags="" + ]) + + # Base flags + AX_APPEND_COMPILE_FLAGS([ dnl + -fno-strict-aliasing dnl + $3 dnl + ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test]) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + # "yes" flags + AX_APPEND_COMPILE_FLAGS([ dnl + -Wall dnl + -Wextra dnl + -Wundef dnl + -Wwrite-strings dnl + -Wpointer-arith dnl + -Wmissing-declarations dnl + -Wredundant-decls dnl + -Wno-unused-parameter dnl + -Wno-missing-field-initializers dnl + -Wformat=2 dnl + -Wcast-align dnl + -Wformat-nonliteral dnl + -Wformat-security dnl + -Wsign-compare dnl + -Wstrict-aliasing dnl + -Wshadow dnl + -Winline dnl + -Wpacked dnl + -Wmissing-format-attribute dnl + -Wmissing-noreturn dnl + -Winit-self dnl + -Wredundant-decls dnl + -Wmissing-include-dirs dnl + -Wunused-but-set-variable dnl + -Warray-bounds dnl + -Wreturn-type dnl + -Wno-overloaded-virtual dnl + -Wswitch-enum dnl + -Wswitch-default dnl + $4 dnl + $5 dnl + $6 dnl + $7 dnl + ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test]) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags; -Werror has to be appended unconditionally because + # it's not possible to test for + # + # suggest-attribute=format is disabled because it gives too many false + # positives + AX_APPEND_FLAG([-Werror],ax_warn_cxxflags_variable) + + AX_APPEND_COMPILE_FLAGS([ dnl + [$ax_compiler_no_suggest_attribute_flags] dnl + ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test]) + ]) + + # In the flags below, when disabling specific flags, always add *both* + # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example) + # we enable -Werror, disable a flag, and a build bot passes CXXFLAGS=-Wall, + # which effectively turns that flag back on again as an error. + for flag in $ax_warn_cxxflags_variable; do + AS_CASE([$flag], + [-Wno-*=*],[], + [-Wno-*],[ + AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')], + ax_warn_cxxflags_variable, + [$ax_compiler_flags_test]) + ]) + done + + AC_LANG_POP([C++]) + + # Substitute the variables + AC_SUBST(ax_warn_cxxflags_variable) +])dnl AX_COMPILER_FLAGS_CXXFLAGS diff --git a/m4/ax_compiler_flags_gir.m4 b/m4/ax_compiler_flags_gir.m4 new file mode 100644 index 0000000..5b4924a --- /dev/null +++ b/m4/ax_compiler_flags_gir.m4 @@ -0,0 +1,60 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_gir.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_GIR([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the g-ir-scanner (from GObject Introspection) to +# VARIABLE, which defaults to WARN_SCANNERFLAGS. VARIABLE is AC_SUBST-ed +# by this macro, but must be manually added to the SCANNERFLAGS variable +# for each GIR target in the code base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2015 Philip Withnall +# +# 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 6 + +AC_DEFUN([AX_COMPILER_FLAGS_GIR],[ + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + + # Variable names + m4_define([ax_warn_scannerflags_variable], + [m4_normalize(ifelse([$1],,[WARN_SCANNERFLAGS],[$1]))]) + + # Base flags + AX_APPEND_FLAG([$3],ax_warn_scannerflags_variable) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + # "yes" flags + AX_APPEND_FLAG([ dnl + --warn-all dnl + $4 dnl + $5 dnl + $6 dnl + $7 dnl + ],ax_warn_scannerflags_variable) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags + AX_APPEND_FLAG([ dnl + --warn-error dnl + ],ax_warn_scannerflags_variable) + ]) + + # Substitute the variables + AC_SUBST(ax_warn_scannerflags_variable) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_compiler_flags_ldflags.m4 b/m4/ax_compiler_flags_ldflags.m4 new file mode 100644 index 0000000..976d119 --- /dev/null +++ b/m4/ax_compiler_flags_ldflags.m4 @@ -0,0 +1,111 @@ +# ============================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_ldflags.html +# ============================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_LDFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the linker to VARIABLE, which defaults to +# WARN_LDFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be +# manually added to the LDFLAGS variable for each target in the code base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2014, 2015 Philip Withnall +# Copyright (c) 2017, 2018 Reini Urban +# +# 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 9 + +AC_DEFUN([AX_COMPILER_FLAGS_LDFLAGS],[ + AX_REQUIRE_DEFINED([AX_APPEND_LINK_FLAGS]) + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG]) + + # Variable names + m4_define([ax_warn_ldflags_variable], + [m4_normalize(ifelse([$1],,[WARN_LDFLAGS],[$1]))]) + + # Always pass -Werror=unknown-warning-option to get Clang to fail on bad + # flags, otherwise they are always appended to the warn_ldflags variable, + # and Clang warns on them for every compilation unit. + # If this is passed to GCC, it will explode, so the flag must be enabled + # conditionally. + AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[ + ax_compiler_flags_test="-Werror=unknown-warning-option" + ],[ + ax_compiler_flags_test="" + ]) + + AX_CHECK_LINK_FLAG([-Wl,--as-needed], [ + AX_APPEND_LINK_FLAGS([-Wl,--as-needed], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + AX_CHECK_LINK_FLAG([-Wl,-z,relro], [ + AX_APPEND_LINK_FLAGS([-Wl,-z,relro], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + AX_CHECK_LINK_FLAG([-Wl,-z,now], [ + AX_APPEND_LINK_FLAGS([-Wl,-z,now], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + AX_CHECK_LINK_FLAG([-Wl,-z,noexecstack], [ + AX_APPEND_LINK_FLAGS([-Wl,-z,noexecstack], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + # textonly, retpolineplt not yet + + # macOS and cygwin linker do not have --as-needed + AX_CHECK_LINK_FLAG([-Wl,--no-as-needed], [ + ax_compiler_flags_as_needed_option="-Wl,--no-as-needed" + ], [ + ax_compiler_flags_as_needed_option="" + ]) + + # macOS linker speaks with a different accent + ax_compiler_flags_fatal_warnings_option="" + AX_CHECK_LINK_FLAG([-Wl,--fatal-warnings], [ + ax_compiler_flags_fatal_warnings_option="-Wl,--fatal-warnings" + ]) + AX_CHECK_LINK_FLAG([-Wl,-fatal_warnings], [ + ax_compiler_flags_fatal_warnings_option="-Wl,-fatal_warnings" + ]) + + # Base flags + AX_APPEND_LINK_FLAGS([ dnl + $ax_compiler_flags_as_needed_option dnl + $3 dnl + ],ax_warn_ldflags_variable,[$ax_compiler_flags_test]) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + # "yes" flags + AX_APPEND_LINK_FLAGS([$4 $5 $6 $7], + ax_warn_ldflags_variable, + [$ax_compiler_flags_test]) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags; -Werror has to be appended unconditionally because + # it's not possible to test for + # + # suggest-attribute=format is disabled because it gives too many false + # positives + AX_APPEND_LINK_FLAGS([ dnl + $ax_compiler_flags_fatal_warnings_option dnl + ],ax_warn_ldflags_variable,[$ax_compiler_flags_test]) + ]) + + # Substitute the variables + AC_SUBST(ax_warn_ldflags_variable) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4 new file mode 100644 index 0000000..9e9eaed --- /dev/null +++ b/m4/ax_cxx_compile_stdcxx.m4 @@ -0,0 +1,948 @@ +# =========================================================================== +# 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 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 +# Copyright (c) 2012 Zack Weinberg +# Copyright (c) 2013 Roy Stogner +# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov +# Copyright (c) 2015 Paul Norman +# Copyright (c) 2015 Moritz Klammler +# Copyright (c) 2016, 2018 Krzesimir Nowak +# +# 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 + +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], [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 + struct check + { + static_assert(sizeof(int) <= sizeof(T), "not big enough"); + }; + + } + + namespace test_final_override + { + + struct Base + { + virtual void f() {} + }; + + struct Derived : public Base + { + virtual void f() override {} + }; + + } + + namespace test_double_right_angle_brackets + { + + template < typename T > + struct check {}; + + typedef check single_type; + typedef check> double_type; + typedef check>> triple_type; + typedef check>>> 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 + { + 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::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == false, ""); + auto ac = c; + auto av = v; + auto sumi = ac + av + 'x'; + auto sumf = ac + av + 1.0; + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::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 + struct sum; + + template + struct sum + { + static constexpr auto value = N0 + sum::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 + using member = typename T::member_type; + + template + void func(...) {} + + template + void func(member*) {} + + void test(); + + void test() { func(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 + { + static constexpr auto value = true; + }; + + int + test() + { + auto x = 0; + static_assert(is_same::value, ""); + static_assert(is_same::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 +#include +#include + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template + int multiply(Args... args) + { + return (args * ... * 1); + } + + template + 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, decltype(foo)>::value); + static_assert(std::is_same::value); + } + + namespace test_typename_in_template_template_parameter + { + + template 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 + 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 + 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 + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair& + { + 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 + Bad + f(T*, T*); + + template + Good + f(T1*, T2*); + + static_assert (std::is_same_v); + + } + + namespace test_inline_variables + { + + template void f(T) + {} + + template 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_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 +# Copyright (c) 2016 Krzesimir Nowak +# +# 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 +# +# 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_is_release.m4 b/m4/ax_is_release.m4 new file mode 100644 index 0000000..9097ddb --- /dev/null +++ b/m4/ax_is_release.m4 @@ -0,0 +1,80 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_is_release.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_IS_RELEASE(POLICY) +# +# DESCRIPTION +# +# Determine whether the code is being configured as a release, or from +# git. Set the ax_is_release variable to 'yes' or 'no'. +# +# If building a release version, it is recommended that the configure +# script disable compiler errors and debug features, by conditionalising +# them on the ax_is_release variable. If building from git, these +# features should be enabled. +# +# The POLICY parameter specifies how ax_is_release is determined. It can +# take the following values: +# +# * git-directory: ax_is_release will be 'no' if a '.git' directory exists +# * minor-version: ax_is_release will be 'no' if the minor version number +# in $PACKAGE_VERSION is odd; this assumes +# $PACKAGE_VERSION follows the 'major.minor.micro' scheme +# * micro-version: ax_is_release will be 'no' if the micro version number +# in $PACKAGE_VERSION is odd; this assumes +# $PACKAGE_VERSION follows the 'major.minor.micro' scheme +# * dash-version: ax_is_release will be 'no' if there is a dash '-' +# in $PACKAGE_VERSION, for example 1.2-pre3, 1.2.42-a8b9 +# or 2.0-dirty (in particular this is suitable for use +# with git-version-gen) +# * always: ax_is_release will always be 'yes' +# * never: ax_is_release will always be 'no' +# +# Other policies may be added in future. +# +# LICENSE +# +# Copyright (c) 2015 Philip Withnall +# Copyright (c) 2016 Collabora Ltd. +# +# 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. + +#serial 7 + +AC_DEFUN([AX_IS_RELEASE],[ + AC_BEFORE([AC_INIT],[$0]) + + m4_case([$1], + [git-directory],[ + # $is_release = (.git directory does not exist) + AS_IF([test -d ${srcdir}/.git],[ax_is_release=no],[ax_is_release=yes]) + ], + [minor-version],[ + # $is_release = ($minor_version is even) + minor_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]][[^.]]*.\([[^.]][[^.]]*\).*/\1/'` + AS_IF([test "$(( $minor_version % 2 ))" -ne 0], + [ax_is_release=no],[ax_is_release=yes]) + ], + [micro-version],[ + # $is_release = ($micro_version is even) + micro_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]]*\.[[^.]]*\.\([[^.]]*\).*/\1/'` + AS_IF([test "$(( $micro_version % 2 ))" -ne 0], + [ax_is_release=no],[ax_is_release=yes]) + ], + [dash-version],[ + # $is_release = ($PACKAGE_VERSION has a dash) + AS_CASE([$PACKAGE_VERSION], + [*-*], [ax_is_release=no], + [*], [ax_is_release=yes]) + ], + [always],[ax_is_release=yes], + [never],[ax_is_release=no], + [ + AC_MSG_ERROR([Invalid policy. Valid policies: git-directory, minor-version, micro-version, dash-version, always, never.]) + ]) +]) diff --git a/m4/ax_lib_readline.m4 b/m4/ax_lib_readline.m4 new file mode 100644 index 0000000..2ca00a3 --- /dev/null +++ b/m4/ax_lib_readline.m4 @@ -0,0 +1,109 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_lib_readline.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_LIB_READLINE +# +# DESCRIPTION +# +# Searches for a readline compatible library. If found, defines +# `HAVE_LIBREADLINE'. If the found library has the `add_history' function, +# sets also `HAVE_READLINE_HISTORY'. Also checks for the locations of the +# necessary include files and sets `HAVE_READLINE_H' or +# `HAVE_READLINE_READLINE_H' and `HAVE_READLINE_HISTORY_H' or +# 'HAVE_HISTORY_H' if the corresponding include files exists. +# +# The libraries that may be readline compatible are `libedit', +# `libeditline' and `libreadline'. Sometimes we need to link a termcap +# library for readline to work, this macro tests these cases too by trying +# to link with `libtermcap', `libcurses' or `libncurses' before giving up. +# +# Here is an example of how to use the information provided by this macro +# to perform the necessary includes or declarations in a C file: +# +# #ifdef HAVE_LIBREADLINE +# # if defined(HAVE_READLINE_READLINE_H) +# # include +# # elif defined(HAVE_READLINE_H) +# # include +# # else /* !defined(HAVE_READLINE_H) */ +# extern char *readline (); +# # endif /* !defined(HAVE_READLINE_H) */ +# char *cmdline = NULL; +# #else /* !defined(HAVE_READLINE_READLINE_H) */ +# /* no readline */ +# #endif /* HAVE_LIBREADLINE */ +# +# #ifdef HAVE_READLINE_HISTORY +# # if defined(HAVE_READLINE_HISTORY_H) +# # include +# # elif defined(HAVE_HISTORY_H) +# # include +# # else /* !defined(HAVE_HISTORY_H) */ +# extern void add_history (); +# extern int write_history (); +# extern int read_history (); +# # endif /* defined(HAVE_READLINE_HISTORY_H) */ +# /* no history */ +# #endif /* HAVE_READLINE_HISTORY */ +# +# LICENSE +# +# Copyright (c) 2008 Ville Laurikari +# +# 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 + +AU_ALIAS([VL_LIB_READLINE], [AX_LIB_READLINE]) +AC_DEFUN([AX_LIB_READLINE], [ + AC_CACHE_CHECK([for a readline compatible library], + ax_cv_lib_readline, [ + ORIG_LIBS="$LIBS" + # djcb: we need the _real_ readline_ + #for readline_lib in readline edit editline; do + for readline_lib in readline; do + for termcap_lib in "" termcap curses ncurses; do + if test -z "$termcap_lib"; then + TRY_LIB="-l$readline_lib" + else + TRY_LIB="-l$readline_lib -l$termcap_lib" + fi + LIBS="$ORIG_LIBS $TRY_LIB" + AC_LINK_IFELSE([AC_LANG_CALL([], [readline])], [ax_cv_lib_readline="$TRY_LIB"]) + if test -n "$ax_cv_lib_readline"; then + break + fi + done + if test -n "$ax_cv_lib_readline"; then + break + fi + done + if test -z "$ax_cv_lib_readline"; then + ax_cv_lib_readline="no" + fi + LIBS="$ORIG_LIBS" + ]) + + if test "$ax_cv_lib_readline" != "no"; then + LIBS="$LIBS $ax_cv_lib_readline" + AC_DEFINE(HAVE_LIBREADLINE, 1, + [Define if you have a readline compatible library]) + AC_CHECK_HEADERS(readline.h readline/readline.h) + AC_CACHE_CHECK([whether readline supports history], + ax_cv_lib_readline_history, [ + ax_cv_lib_readline_history="no" + AC_LINK_IFELSE([AC_LANG_CALL([], [add_history])], [ax_cv_lib_readline_history="yes"]) + ]) + if test "$ax_cv_lib_readline_history" = "yes"; then + AC_DEFINE(HAVE_READLINE_HISTORY, 1, + [Define if your readline library has \`add_history']) + AC_CHECK_HEADERS(history.h readline/history.h) + fi + fi +])dnl diff --git a/m4/ax_require_defined.m4 b/m4/ax_require_defined.m4 new file mode 100644 index 0000000..17c3eab --- /dev/null +++ b/m4/ax_require_defined.m4 @@ -0,0 +1,37 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_require_defined.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_REQUIRE_DEFINED(MACRO) +# +# DESCRIPTION +# +# AX_REQUIRE_DEFINED is a simple helper for making sure other macros have +# been defined and thus are available for use. This avoids random issues +# where a macro isn't expanded. Instead the configure script emits a +# non-fatal: +# +# ./configure: line 1673: AX_CFLAGS_WARN_ALL: command not found +# +# It's like AC_REQUIRE except it doesn't expand the required macro. +# +# Here's an example: +# +# AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG]) +# +# LICENSE +# +# Copyright (c) 2014 Mike Frysinger +# +# 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 + +AC_DEFUN([AX_REQUIRE_DEFINED], [dnl + m4_ifndef([$1], [m4_fatal([macro ]$1[ is not defined; is a m4 file missing?])]) +])dnl AX_REQUIRE_DEFINED diff --git a/m4/ax_valgrind_check.m4 b/m4/ax_valgrind_check.m4 new file mode 100644 index 0000000..7033798 --- /dev/null +++ b/m4/ax_valgrind_check.m4 @@ -0,0 +1,239 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_valgrind_check.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_VALGRIND_DFLT(memcheck|helgrind|drd|sgcheck, on|off) +# AX_VALGRIND_CHECK() +# +# DESCRIPTION +# +# AX_VALGRIND_CHECK checks whether Valgrind is present and, if so, allows +# running `make check` under a variety of Valgrind tools to check for +# memory and threading errors. +# +# Defines VALGRIND_CHECK_RULES which should be substituted in your +# Makefile; and $enable_valgrind which can be used in subsequent configure +# output. VALGRIND_ENABLED is defined and substituted, and corresponds to +# the value of the --enable-valgrind option, which defaults to being +# enabled if Valgrind is installed and disabled otherwise. Individual +# Valgrind tools can be disabled via --disable-valgrind-, the +# default is configurable via the AX_VALGRIND_DFLT command or is to use +# all commands not disabled via AX_VALGRIND_DFLT. All AX_VALGRIND_DFLT +# calls must be made before the call to AX_VALGRIND_CHECK. +# +# If unit tests are written using a shell script and automake's +# LOG_COMPILER system, the $(VALGRIND) variable can be used within the +# shell scripts to enable Valgrind, as described here: +# +# https://www.gnu.org/software/gnulib/manual/html_node/Running-self_002dtests-under-valgrind.html +# +# Usage example: +# +# configure.ac: +# +# AX_VALGRIND_DFLT([sgcheck], [off]) +# AX_VALGRIND_CHECK +# +# in each Makefile.am with tests: +# +# @VALGRIND_CHECK_RULES@ +# VALGRIND_SUPPRESSIONS_FILES = my-project.supp +# EXTRA_DIST = my-project.supp +# +# This results in a "check-valgrind" rule being added. Running `make +# check-valgrind` in that directory will recursively run the module's test +# suite (`make check`) once for each of the available Valgrind tools (out +# of memcheck, helgrind and drd) while the sgcheck will be skipped unless +# enabled again on the commandline with --enable-valgrind-sgcheck. The +# results for each check will be output to test-suite-$toolname.log. The +# target will succeed if there are zero errors and fail otherwise. +# +# Alternatively, a "check-valgrind-$TOOL" rule will be added, for $TOOL in +# memcheck, helgrind, drd and sgcheck. These are useful because often only +# some of those tools can be ran cleanly on a codebase. +# +# The macro supports running with and without libtool. +# +# LICENSE +# +# Copyright (c) 2014, 2015, 2016 Philip Withnall +# +# 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 + +dnl Configured tools +m4_define([valgrind_tool_list], [[memcheck], [helgrind], [drd], [sgcheck]]) +m4_set_add_all([valgrind_exp_tool_set], [sgcheck]) +m4_foreach([vgtool], [valgrind_tool_list], + [m4_define([en_dflt_valgrind_]vgtool, [on])]) + +AC_DEFUN([AX_VALGRIND_DFLT],[ + m4_define([en_dflt_valgrind_$1], [$2]) +])dnl + +AM_EXTRA_RECURSIVE_TARGETS([check-valgrind]) +m4_foreach([vgtool], [valgrind_tool_list], + [AM_EXTRA_RECURSIVE_TARGETS([check-valgrind-]vgtool)]) + +AC_DEFUN([AX_VALGRIND_CHECK],[ + dnl Check for --enable-valgrind + AC_ARG_ENABLE([valgrind], + [AS_HELP_STRING([--enable-valgrind], [Whether to enable Valgrind on the unit tests])], + [enable_valgrind=$enableval],[enable_valgrind=]) + + AS_IF([test "$enable_valgrind" != "no"],[ + # Check for Valgrind. + AC_CHECK_PROG([VALGRIND],[valgrind],[valgrind]) + AS_IF([test "$VALGRIND" = ""],[ + AS_IF([test "$enable_valgrind" = "yes"],[ + AC_MSG_ERROR([Could not find valgrind; either install it or reconfigure with --disable-valgrind]) + ],[ + enable_valgrind=no + ]) + ],[ + enable_valgrind=yes + ]) + ]) + + AM_CONDITIONAL([VALGRIND_ENABLED],[test "$enable_valgrind" = "yes"]) + AC_SUBST([VALGRIND_ENABLED],[$enable_valgrind]) + + # Check for Valgrind tools we care about. + [valgrind_enabled_tools=] + m4_foreach([vgtool],[valgrind_tool_list],[ + AC_ARG_ENABLE([valgrind-]vgtool, + m4_if(m4_defn([en_dflt_valgrind_]vgtool),[off],dnl +[AS_HELP_STRING([--enable-valgrind-]vgtool, [Whether to use ]vgtool[ during the Valgrind tests])],dnl +[AS_HELP_STRING([--disable-valgrind-]vgtool, [Whether to skip ]vgtool[ during the Valgrind tests])]), + [enable_valgrind_]vgtool[=$enableval], + [enable_valgrind_]vgtool[=]) + AS_IF([test "$enable_valgrind" = "no"],[ + enable_valgrind_]vgtool[=no], + [test "$enable_valgrind_]vgtool[" ]dnl +m4_if(m4_defn([en_dflt_valgrind_]vgtool), [off], [= "yes"], [!= "no"]),[ + AC_CACHE_CHECK([for Valgrind tool ]vgtool, + [ax_cv_valgrind_tool_]vgtool,[ + ax_cv_valgrind_tool_]vgtool[=no + m4_set_contains([valgrind_exp_tool_set],vgtool, + [m4_define([vgtoolx],[exp-]vgtool)], + [m4_define([vgtoolx],vgtool)]) + AS_IF([`$VALGRIND --tool=]vgtoolx[ --help >/dev/null 2>&1`],[ + ax_cv_valgrind_tool_]vgtool[=yes + ]) + ]) + AS_IF([test "$ax_cv_valgrind_tool_]vgtool[" = "no"],[ + AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[ + AC_MSG_ERROR([Valgrind does not support ]vgtool[; reconfigure with --disable-valgrind-]vgtool) + ],[ + enable_valgrind_]vgtool[=no + ]) + ],[ + enable_valgrind_]vgtool[=yes + ]) + ]) + AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[ + valgrind_enabled_tools="$valgrind_enabled_tools ]m4_bpatsubst(vgtool,[^exp-])[" + ]) + AC_SUBST([ENABLE_VALGRIND_]vgtool,[$enable_valgrind_]vgtool) + ]) + AC_SUBST([valgrind_tools],["]m4_join([ ], valgrind_tool_list)["]) + AC_SUBST([valgrind_enabled_tools],[$valgrind_enabled_tools]) + +[VALGRIND_CHECK_RULES=' +# Valgrind check +# +# Optional: +# - VALGRIND_SUPPRESSIONS_FILES: Space-separated list of Valgrind suppressions +# files to load. (Default: empty) +# - VALGRIND_FLAGS: General flags to pass to all Valgrind tools. +# (Default: --num-callers=30) +# - VALGRIND_$toolname_FLAGS: Flags to pass to Valgrind $toolname (one of: +# memcheck, helgrind, drd, sgcheck). (Default: various) + +# Optional variables +VALGRIND_SUPPRESSIONS ?= $(addprefix --suppressions=,$(VALGRIND_SUPPRESSIONS_FILES)) +VALGRIND_FLAGS ?= --num-callers=30 +VALGRIND_memcheck_FLAGS ?= --leak-check=full --show-reachable=no +VALGRIND_helgrind_FLAGS ?= --history-level=approx +VALGRIND_drd_FLAGS ?= +VALGRIND_sgcheck_FLAGS ?= + +# Internal use +valgrind_log_files = $(addprefix test-suite-,$(addsuffix .log,$(valgrind_tools))) + +valgrind_memcheck_flags = --tool=memcheck $(VALGRIND_memcheck_FLAGS) +valgrind_helgrind_flags = --tool=helgrind $(VALGRIND_helgrind_FLAGS) +valgrind_drd_flags = --tool=drd $(VALGRIND_drd_FLAGS) +valgrind_sgcheck_flags = --tool=exp-sgcheck $(VALGRIND_sgcheck_FLAGS) + +valgrind_quiet = $(valgrind_quiet_$(V)) +valgrind_quiet_ = $(valgrind_quiet_$(AM_DEFAULT_VERBOSITY)) +valgrind_quiet_0 = --quiet +valgrind_v_use = $(valgrind_v_use_$(V)) +valgrind_v_use_ = $(valgrind_v_use_$(AM_DEFAULT_VERBOSITY)) +valgrind_v_use_0 = @echo " USE " $(patsubst check-valgrind-%-am,%,$''@):; + +# Support running with and without libtool. +ifneq ($(LIBTOOL),) +valgrind_lt = $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=execute +else +valgrind_lt = +endif + +# Use recursive makes in order to ignore errors during check +check-valgrind-am: +ifeq ($(VALGRIND_ENABLED),yes) + $(A''M_V_at)$(MAKE) $(AM_MAKEFLAGS) -k \ + $(foreach tool, $(valgrind_enabled_tools), check-valgrind-$(tool)) +else + @echo "Need to reconfigure with --enable-valgrind" +endif + +# Valgrind running +VALGRIND_TESTS_ENVIRONMENT = \ + $(TESTS_ENVIRONMENT) \ + env VALGRIND=$(VALGRIND) \ + G_SLICE=always-malloc,debug-blocks \ + G_DEBUG=fatal-warnings,fatal-criticals,gc-friendly + +VALGRIND_LOG_COMPILER = \ + $(valgrind_lt) \ + $(VALGRIND) $(VALGRIND_SUPPRESSIONS) --error-exitcode=1 $(VALGRIND_FLAGS) + +define valgrind_tool_rule +check-valgrind-$(1)-am: +ifeq ($$(VALGRIND_ENABLED)-$$(ENABLE_VALGRIND_$(1)),yes-yes) +ifneq ($$(TESTS),) + $$(valgrind_v_use)$$(MAKE) check-TESTS \ + TESTS_ENVIRONMENT="$$(VALGRIND_TESTS_ENVIRONMENT)" \ + LOG_COMPILER="$$(VALGRIND_LOG_COMPILER)" \ + LOG_FLAGS="$$(valgrind_$(1)_flags)" \ + TEST_SUITE_LOG=test-suite-$(1).log +endif +else ifeq ($$(VALGRIND_ENABLED),yes) + @echo "Need to reconfigure with --enable-valgrind-$(1)" +else + @echo "Need to reconfigure with --enable-valgrind" +endif +endef + +$(foreach tool,$(valgrind_tools),$(eval $(call valgrind_tool_rule,$(tool)))) + +A''M_DISTCHECK_CONFIGURE_FLAGS ?= +A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-valgrind + +MOSTLYCLEANFILES ?= +MOSTLYCLEANFILES += $(valgrind_log_files) + +.PHONY: check-valgrind $(add-prefix check-valgrind-,$(valgrind_tools)) +'] + + AC_SUBST([VALGRIND_CHECK_RULES]) + m4_ifdef([_AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE([VALGRIND_CHECK_RULES])]) +]) diff --git a/m4/guile.m4 b/m4/guile.m4 new file mode 100644 index 0000000..6968973 --- /dev/null +++ b/m4/guile.m4 @@ -0,0 +1,397 @@ +## Autoconf macros for working with Guile. +## +## Copyright (C) 1998,2001, 2006, 2010, 2012, 2013, 2014 Free Software Foundation, Inc. +## +## 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 3 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 library; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +## 02110-1301 USA + +# serial 10 + +## Index +## ----- +## +## GUILE_PKG -- find Guile development files +## GUILE_PROGS -- set paths to Guile interpreter, config and tool programs +## GUILE_FLAGS -- set flags for compiling and linking with Guile +## GUILE_SITE_DIR -- find path to Guile "site" directories +## GUILE_CHECK -- evaluate Guile Scheme code and capture the return value +## GUILE_MODULE_CHECK -- check feature of a Guile Scheme module +## GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module +## GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable +## GUILE_MODULE_EXPORTS -- check if a module exports a variable +## GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable + +## Code +## ---- + +## NOTE: Comments preceding an AC_DEFUN (starting from "Usage:") are massaged +## into doc/ref/autoconf-macros.texi (see Makefile.am in that directory). + +# GUILE_PKG -- find Guile development files +# +# Usage: GUILE_PKG([VERSIONS]) +# +# This macro runs the @code{pkg-config} tool to find development files +# for an available version of Guile. +# +# By default, this macro will search for the latest stable version of +# Guile (e.g. 3.0), falling back to the previous stable version +# (e.g. 2.2) if it is available. If no guile-@var{VERSION}.pc file is +# found, an error is signalled. The found version is stored in +# @var{GUILE_EFFECTIVE_VERSION}. +# +# If @code{GUILE_PROGS} was already invoked, this macro ensures that the +# development files have the same effective version as the Guile +# program. +# +# @var{GUILE_EFFECTIVE_VERSION} is marked for substitution, as by +# @code{AC_SUBST}. +# +AC_DEFUN([GUILE_PKG], + [AC_REQUIRE([PKG_PROG_PKG_CONFIG]) + if test "x$PKG_CONFIG" = x; then + AC_MSG_ERROR([pkg-config is missing, please install it]) + fi + _guile_versions_to_search="m4_default([$1], [3.0 2.2 2.0])" + if test -n "$GUILE_EFFECTIVE_VERSION"; then + _guile_tmp="" + for v in $_guile_versions_to_search; do + if test "$v" = "$GUILE_EFFECTIVE_VERSION"; then + _guile_tmp=$v + fi + done + if test -z "$_guile_tmp"; then + AC_MSG_FAILURE([searching for guile development files for versions $_guile_versions_to_search, but previously found $GUILE version $GUILE_EFFECTIVE_VERSION]) + fi + _guile_versions_to_search=$GUILE_EFFECTIVE_VERSION + fi + GUILE_EFFECTIVE_VERSION="" + _guile_errors="" + for v in $_guile_versions_to_search; do + if test -z "$GUILE_EFFECTIVE_VERSION"; then + AC_MSG_NOTICE([checking for guile $v]) + PKG_CHECK_EXISTS([guile-$v], [GUILE_EFFECTIVE_VERSION=$v], []) + fi + done + + if test -z "$GUILE_EFFECTIVE_VERSION"; then + AC_MSG_ERROR([ +No Guile development packages were found. + +Please verify that you have Guile installed. If you installed Guile +from a binary distribution, please verify that you have also installed +the development packages. If you installed it yourself, you might need +to adjust your PKG_CONFIG_PATH; see the pkg-config man page for more. +]) + fi + AC_MSG_NOTICE([found guile $GUILE_EFFECTIVE_VERSION]) + AC_SUBST([GUILE_EFFECTIVE_VERSION]) + ]) + +# GUILE_FLAGS -- set flags for compiling and linking with Guile +# +# Usage: GUILE_FLAGS +# +# This macro runs the @code{pkg-config} tool to find out how to compile +# and link programs against Guile. It sets four variables: +# @var{GUILE_CFLAGS}, @var{GUILE_LDFLAGS}, @var{GUILE_LIBS}, and +# @var{GUILE_LTLIBS}. +# +# @var{GUILE_CFLAGS}: flags to pass to a C or C++ compiler to build code that +# uses Guile header files. This is almost always just one or more @code{-I} +# flags. +# +# @var{GUILE_LDFLAGS}: flags to pass to the compiler to link a program +# against Guile. This includes @code{-lguile-@var{VERSION}} for the +# Guile library itself, and may also include one or more @code{-L} flag +# to tell the compiler where to find the libraries. But it does not +# include flags that influence the program's runtime search path for +# libraries, and will therefore lead to a program that fails to start, +# unless all necessary libraries are installed in a standard location +# such as @file{/usr/lib}. +# +# @var{GUILE_LIBS} and @var{GUILE_LTLIBS}: flags to pass to the compiler or to +# libtool, respectively, to link a program against Guile. It includes flags +# that augment the program's runtime search path for libraries, so that shared +# libraries will be found at the location where they were during linking, even +# in non-standard locations. @var{GUILE_LIBS} is to be used when linking the +# program directly with the compiler, whereas @var{GUILE_LTLIBS} is to be used +# when linking the program is done through libtool. +# +# The variables are marked for substitution, as by @code{AC_SUBST}. +# +AC_DEFUN([GUILE_FLAGS], + [AC_REQUIRE([GUILE_PKG]) + PKG_CHECK_MODULES(GUILE, [guile-$GUILE_EFFECTIVE_VERSION]) + + dnl GUILE_CFLAGS and GUILE_LIBS are already defined and AC_SUBST'd by + dnl PKG_CHECK_MODULES. But GUILE_LIBS to pkg-config is GUILE_LDFLAGS + dnl to us. + + GUILE_LDFLAGS=$GUILE_LIBS + + dnl Determine the platform dependent parameters needed to use rpath. + dnl AC_LIB_LINKFLAGS_FROM_LIBS is defined in gnulib/m4/lib-link.m4 and needs + dnl the file gnulib/build-aux/config.rpath. + AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LIBS], [$GUILE_LDFLAGS], []) + GUILE_LIBS="$GUILE_LDFLAGS $GUILE_LIBS" + AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LTLIBS], [$GUILE_LDFLAGS], [yes]) + GUILE_LTLIBS="$GUILE_LDFLAGS $GUILE_LTLIBS" + + AC_SUBST([GUILE_EFFECTIVE_VERSION]) + AC_SUBST([GUILE_CFLAGS]) + AC_SUBST([GUILE_LDFLAGS]) + AC_SUBST([GUILE_LIBS]) + AC_SUBST([GUILE_LTLIBS]) + ]) + +# GUILE_SITE_DIR -- find path to Guile site directories +# +# Usage: GUILE_SITE_DIR +# +# This looks for Guile's "site" directories. The variable @var{GUILE_SITE} will +# be set to Guile's "site" directory for Scheme source files (usually something +# like PREFIX/share/guile/site). @var{GUILE_SITE_CCACHE} will be set to the +# directory for compiled Scheme files also known as @code{.go} files +# (usually something like +# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/site-ccache). +# @var{GUILE_EXTENSION} will be set to the directory for compiled C extensions +# (usually something like +# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/extensions). The latter two +# are set to blank if the particular version of Guile does not support +# them. Note that this macro will run the macros @code{GUILE_PKG} and +# @code{GUILE_PROGS} if they have not already been run. +# +# The variables are marked for substitution, as by @code{AC_SUBST}. +# +AC_DEFUN([GUILE_SITE_DIR], + [AC_REQUIRE([GUILE_PKG]) + AC_REQUIRE([GUILE_PROGS]) + AC_MSG_CHECKING(for Guile site directory) + GUILE_SITE=`$PKG_CONFIG --print-errors --variable=sitedir guile-$GUILE_EFFECTIVE_VERSION` + AC_MSG_RESULT($GUILE_SITE) + if test "$GUILE_SITE" = ""; then + AC_MSG_FAILURE(sitedir not found) + fi + AC_SUBST(GUILE_SITE) + AC_MSG_CHECKING([for Guile site-ccache directory using pkgconfig]) + GUILE_SITE_CCACHE=`$PKG_CONFIG --variable=siteccachedir guile-$GUILE_EFFECTIVE_VERSION` + if test "$GUILE_SITE_CCACHE" = ""; then + AC_MSG_RESULT(no) + AC_MSG_CHECKING([for Guile site-ccache directory using interpreter]) + GUILE_SITE_CCACHE=`$GUILE -c "(display (if (defined? '%site-ccache-dir) (%site-ccache-dir) \"\"))"` + if test $? != "0" -o "$GUILE_SITE_CCACHE" = ""; then + AC_MSG_RESULT(no) + GUILE_SITE_CCACHE="" + AC_MSG_WARN([siteccachedir not found]) + fi + fi + AC_MSG_RESULT($GUILE_SITE_CCACHE) + AC_SUBST([GUILE_SITE_CCACHE]) + AC_MSG_CHECKING(for Guile extensions directory) + GUILE_EXTENSION=`$PKG_CONFIG --print-errors --variable=extensiondir guile-$GUILE_EFFECTIVE_VERSION` + AC_MSG_RESULT($GUILE_EXTENSION) + if test "$GUILE_EXTENSION" = ""; then + GUILE_EXTENSION="" + AC_MSG_WARN(extensiondir not found) + fi + AC_SUBST(GUILE_EXTENSION) + ]) + +# GUILE_PROGS -- set paths to Guile interpreter, config and tool programs +# +# Usage: GUILE_PROGS([VERSION]) +# +# This macro looks for programs @code{guile} and @code{guild}, setting +# variables @var{GUILE} and @var{GUILD} to their paths, respectively. +# The macro will attempt to find @code{guile} with the suffix of +# @code{-X.Y}, followed by looking for it with the suffix @code{X.Y}, and +# then fall back to looking for @code{guile} with no suffix. If +# @code{guile} is still not found, signal an error. The suffix, if any, +# that was required to find @code{guile} will be used for @code{guild} +# as well. +# +# By default, this macro will search for the latest stable version of +# Guile (e.g. 3.0). x.y or x.y.z versions can be specified. If an older +# version is found, the macro will signal an error. +# +# The effective version of the found @code{guile} is set to +# @var{GUILE_EFFECTIVE_VERSION}. This macro ensures that the effective +# version is compatible with the result of a previous invocation of +# @code{GUILE_FLAGS}, if any. +# +# As a legacy interface, it also looks for @code{guile-config} and +# @code{guile-tools}, setting @var{GUILE_CONFIG} and @var{GUILE_TOOLS}. +# +# The variables are marked for substitution, as by @code{AC_SUBST}. +# +AC_DEFUN([GUILE_PROGS], + [_guile_required_version="m4_default([$1], [$GUILE_EFFECTIVE_VERSION])" + if test -z "$_guile_required_version"; then + _guile_required_version=3.0 + fi + + _guile_candidates=guile + _tmp= + for v in `echo "$_guile_required_version" | tr . ' '`; do + if test -n "$_tmp"; then _tmp=$_tmp.; fi + _tmp=$_tmp$v + _guile_candidates="guile-$_tmp guile$_tmp $_guile_candidates" + done + + AC_PATH_PROGS(GUILE,[$_guile_candidates]) + if test -z "$GUILE"; then + AC_MSG_ERROR([guile required but not found]) + fi + + _guile_suffix=`echo "$GUILE" | sed -e 's,^.*/guile\(.*\)$,\1,'` + _guile_effective_version=`$GUILE -c "(display (effective-version))"` + if test -z "$GUILE_EFFECTIVE_VERSION"; then + GUILE_EFFECTIVE_VERSION=$_guile_effective_version + elif test "$GUILE_EFFECTIVE_VERSION" != "$_guile_effective_version"; then + AC_MSG_ERROR([found development files for Guile $GUILE_EFFECTIVE_VERSION, but $GUILE has effective version $_guile_effective_version]) + fi + + _guile_major_version=`$GUILE -c "(display (major-version))"` + _guile_minor_version=`$GUILE -c "(display (minor-version))"` + _guile_micro_version=`$GUILE -c "(display (micro-version))"` + _guile_prog_version="$_guile_major_version.$_guile_minor_version.$_guile_micro_version" + + AC_MSG_CHECKING([for Guile version >= $_guile_required_version]) + _major_version=`echo $_guile_required_version | cut -d . -f 1` + _minor_version=`echo $_guile_required_version | cut -d . -f 2` + _micro_version=`echo $_guile_required_version | cut -d . -f 3` + if test "$_guile_major_version" -gt "$_major_version"; then + true + elif test "$_guile_major_version" -eq "$_major_version"; then + if test "$_guile_minor_version" -gt "$_minor_version"; then + true + elif test "$_guile_minor_version" -eq "$_minor_version"; then + if test -n "$_micro_version"; then + if test "$_guile_micro_version" -lt "$_micro_version"; then + AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found]) + fi + fi + elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then + # Allow prereleases that have the right effective version. + true + else + as_fn_error $? "Guile $_guile_required_version required, but $_guile_prog_version found" "$LINENO" 5 + fi + elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then + # Allow prereleases that have the right effective version. + true + else + AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found]) + fi + AC_MSG_RESULT([$_guile_prog_version]) + + AC_PATH_PROG(GUILD,[guild$_guile_suffix]) + AC_SUBST(GUILD) + + AC_PATH_PROG(GUILE_CONFIG,[guile-config$_guile_suffix]) + AC_SUBST(GUILE_CONFIG) + if test -n "$GUILD"; then + GUILE_TOOLS=$GUILD + else + AC_PATH_PROG(GUILE_TOOLS,[guile-tools$_guile_suffix]) + fi + AC_SUBST(GUILE_TOOLS) + ]) + +# GUILE_CHECK -- evaluate Guile Scheme code and capture the return value +# +# Usage: GUILE_CHECK_RETVAL(var,check) +# +# @var{var} is a shell variable name to be set to the return value. +# @var{check} is a Guile Scheme expression, evaluated with "$GUILE -c", and +# returning either 0 or non-#f to indicate the check passed. +# Non-0 number or #f indicates failure. +# Avoid using the character "#" since that confuses autoconf. +# +AC_DEFUN([GUILE_CHECK], + [AC_REQUIRE([GUILE_PROGS]) + $GUILE -c "$2" > /dev/null 2>&1 + $1=$? + ]) + +# GUILE_MODULE_CHECK -- check feature of a Guile Scheme module +# +# Usage: GUILE_MODULE_CHECK(var,module,featuretest,description) +# +# @var{var} is a shell variable name to be set to "yes" or "no". +# @var{module} is a list of symbols, like: (ice-9 common-list). +# @var{featuretest} is an expression acceptable to GUILE_CHECK, q.v. +# @var{description} is a present-tense verb phrase (passed to AC_MSG_CHECKING). +# +AC_DEFUN([GUILE_MODULE_CHECK], + [AC_MSG_CHECKING([if $2 $4]) + GUILE_CHECK($1,(use-modules $2) (exit ((lambda () $3)))) + if test "$$1" = "0" ; then $1=yes ; else $1=no ; fi + AC_MSG_RESULT($$1) + ]) + +# GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module +# +# Usage: GUILE_MODULE_AVAILABLE(var,module) +# +# @var{var} is a shell variable name to be set to "yes" or "no". +# @var{module} is a list of symbols, like: (ice-9 common-list). +# +AC_DEFUN([GUILE_MODULE_AVAILABLE], + [GUILE_MODULE_CHECK($1,$2,0,is available) + ]) + +# GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable +# +# Usage: GUILE_MODULE_REQUIRED(symlist) +# +# @var{symlist} is a list of symbols, WITHOUT surrounding parens, +# like: ice-9 common-list. +# +AC_DEFUN([GUILE_MODULE_REQUIRED], + [GUILE_MODULE_AVAILABLE(ac_guile_module_required, ($1)) + if test "$ac_guile_module_required" = "no" ; then + AC_MSG_ERROR([required guile module not found: ($1)]) + fi + ]) + +# GUILE_MODULE_EXPORTS -- check if a module exports a variable +# +# Usage: GUILE_MODULE_EXPORTS(var,module,modvar) +# +# @var{var} is a shell variable to be set to "yes" or "no". +# @var{module} is a list of symbols, like: (ice-9 common-list). +# @var{modvar} is the Guile Scheme variable to check. +# +AC_DEFUN([GUILE_MODULE_EXPORTS], + [GUILE_MODULE_CHECK($1,$2,$3,exports `$3') + ]) + +# GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable +# +# Usage: GUILE_MODULE_REQUIRED_EXPORT(module,modvar) +# +# @var{module} is a list of symbols, like: (ice-9 common-list). +# @var{modvar} is the Guile Scheme variable to check. +# +AC_DEFUN([GUILE_MODULE_REQUIRED_EXPORT], + [GUILE_MODULE_EXPORTS(guile_module_required_export,$1,$2) + if test "$guile_module_required_export" = "no" ; then + AC_MSG_ERROR([module $1 does not export $2; required]) + fi + ]) + +## guile.m4 ends here diff --git a/m4/host-cpu-c-abi.m4 b/m4/host-cpu-c-abi.m4 new file mode 100644 index 0000000..6db2aa2 --- /dev/null +++ b/m4/host-cpu-c-abi.m4 @@ -0,0 +1,675 @@ +# host-cpu-c-abi.m4 serial 13 +dnl Copyright (C) 2002-2020 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Bruno Haible and Sam Steingold. + +dnl Sets the HOST_CPU variable to the canonical name of the CPU. +dnl Sets the HOST_CPU_C_ABI variable to the canonical name of the CPU with its +dnl C language ABI (application binary interface). +dnl Also defines __${HOST_CPU}__ and __${HOST_CPU_C_ABI}__ as C macros in +dnl config.h. +dnl +dnl This canonical name can be used to select a particular assembly language +dnl source file that will interoperate with C code on the given host. +dnl +dnl For example: +dnl * 'i386' and 'sparc' are different canonical names, because code for i386 +dnl will not run on SPARC CPUs and vice versa. They have different +dnl instruction sets. +dnl * 'sparc' and 'sparc64' are different canonical names, because code for +dnl 'sparc' and code for 'sparc64' cannot be linked together: 'sparc' code +dnl contains 32-bit instructions, whereas 'sparc64' code contains 64-bit +dnl instructions. A process on a SPARC CPU can be in 32-bit mode or in 64-bit +dnl mode, but not both. +dnl * 'mips' and 'mipsn32' are different canonical names, because they use +dnl different argument passing and return conventions for C functions, and +dnl although the instruction set of 'mips' is a large subset of the +dnl instruction set of 'mipsn32'. +dnl * 'mipsn32' and 'mips64' are different canonical names, because they use +dnl different sizes for the C types like 'int' and 'void *', and although +dnl the instruction sets of 'mipsn32' and 'mips64' are the same. +dnl * The same canonical name is used for different endiannesses. You can +dnl determine the endianness through preprocessor symbols: +dnl - 'arm': test __ARMEL__. +dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL. +dnl - 'powerpc64': test _BIG_ENDIAN vs. _LITTLE_ENDIAN. +dnl * The same name 'i386' is used for CPUs of type i386, i486, i586 +dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because +dnl - Instructions that do not exist on all of these CPUs (cmpxchg, +dnl MMX, SSE, SSE2, 3DNow! etc.) are not frequently used. If your +dnl assembly language source files use such instructions, you will +dnl need to make the distinction. +dnl - Speed of execution of the common instruction set is reasonable across +dnl the entire family of CPUs. If you have assembly language source files +dnl that are optimized for particular CPU types (like GNU gmp has), you +dnl will need to make the distinction. +dnl See . +AC_DEFUN([gl_HOST_CPU_C_ABI], +[ + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([gl_C_ASM]) + AC_CACHE_CHECK([host CPU and C ABI], [gl_cv_host_cpu_c_abi], + [case "$host_cpu" in + +changequote(,)dnl + i[34567]86 ) +changequote([,])dnl + gl_cv_host_cpu_c_abi=i386 + ;; + + x86_64 ) + # On x86_64 systems, the C compiler may be generating code in one of + # these ABIs: + # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64. + # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64 + # with native Windows (mingw, MSVC). + # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32. + # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if (defined __x86_64__ || defined __amd64__ \ + || defined _M_X64 || defined _M_AMD64) + int ok; + #else + error fail + #endif + ]])], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __ILP32__ || defined _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=x86_64-x32], + [gl_cv_host_cpu_c_abi=x86_64])], + [gl_cv_host_cpu_c_abi=i386]) + ;; + +changequote(,)dnl + alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] ) +changequote([,])dnl + gl_cv_host_cpu_c_abi=alpha + ;; + + arm* | aarch64 ) + # Assume arm with EABI. + # On arm64 systems, the C compiler may be generating code in one of + # these ABIs: + # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64. + # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32. + # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef __aarch64__ + int ok; + #else + error fail + #endif + ]])], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __ILP32__ || defined _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=arm64-ilp32], + [gl_cv_host_cpu_c_abi=arm64])], + [# Don't distinguish little-endian and big-endian arm, since they + # don't require different machine code for simple operations and + # since the user can distinguish them through the preprocessor + # defines __ARMEL__ vs. __ARMEB__. + # But distinguish arm which passes floating-point arguments and + # return values in integer registers (r0, r1, ...) - this is + # gcc -mfloat-abi=soft or gcc -mfloat-abi=softfp - from arm which + # passes them in float registers (s0, s1, ...) and double registers + # (d0, d1, ...) - this is gcc -mfloat-abi=hard. GCC 4.6 or newer + # sets the preprocessor defines __ARM_PCS (for the first case) and + # __ARM_PCS_VFP (for the second case), but older GCC does not. + echo 'double ddd; void func (double dd) { ddd = dd; }' > conftest.c + # Look for a reference to the register d0 in the .s file. + AC_TRY_COMMAND(${CC-cc} $CFLAGS $CPPFLAGS $gl_c_asm_opt conftest.c) >/dev/null 2>&1 + if LC_ALL=C grep 'd0,' conftest.$gl_asmext >/dev/null; then + gl_cv_host_cpu_c_abi=armhf + else + gl_cv_host_cpu_c_abi=arm + fi + rm -f conftest* + ]) + ;; + + hppa1.0 | hppa1.1 | hppa2.0* | hppa64 ) + # On hppa, the C compiler may be generating 32-bit code or 64-bit + # code. In the latter case, it defines _LP64 and __LP64__. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef __LP64__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=hppa64], + [gl_cv_host_cpu_c_abi=hppa]) + ;; + + ia64* ) + # On ia64 on HP-UX, the C compiler may be generating 64-bit code or + # 32-bit code. In the latter case, it defines _ILP32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=ia64-ilp32], + [gl_cv_host_cpu_c_abi=ia64]) + ;; + + mips* ) + # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this + # at 32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64) + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=mips64], + [# In the n32 ABI, _ABIN32 is defined, _ABIO32 is not defined (but + # may later get defined by ), and _MIPS_SIM == _ABIN32. + # In the 32 ABI, _ABIO32 is defined, _ABIN32 is not defined (but + # may later get defined by ), and _MIPS_SIM == _ABIO32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if (_MIPS_SIM == _ABIN32) + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=mipsn32], + [gl_cv_host_cpu_c_abi=mips])]) + ;; + + powerpc* ) + # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD. + # No need to distinguish them here; the caller may distinguish + # them based on the OS. + # On powerpc64 systems, the C compiler may still be generating + # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may + # be generating 64-bit code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __powerpc64__ || defined _ARCH_PPC64 + int ok; + #else + error fail + #endif + ]])], + [# On powerpc64, there are two ABIs on Linux: The AIX compatible + # one and the ELFv2 one. The latter defines _CALL_ELF=2. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined _CALL_ELF && _CALL_ELF == 2 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=powerpc64-elfv2], + [gl_cv_host_cpu_c_abi=powerpc64]) + ], + [gl_cv_host_cpu_c_abi=powerpc]) + ;; + + rs6000 ) + gl_cv_host_cpu_c_abi=powerpc + ;; + + riscv32 | riscv64 ) + # There are 2 architectures (with variants): rv32* and rv64*. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if __riscv_xlen == 64 + int ok; + #else + error fail + #endif + ]])], + [cpu=riscv64], + [cpu=riscv32]) + # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d. + # Size of 'long' and 'void *': + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __LP64__ + int ok; + #else + error fail + #endif + ]])], + [main_abi=lp64], + [main_abi=ilp32]) + # Float ABIs: + # __riscv_float_abi_double: + # 'float' and 'double' are passed in floating-point registers. + # __riscv_float_abi_single: + # 'float' are passed in floating-point registers. + # __riscv_float_abi_soft: + # No values are passed in floating-point registers. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __riscv_float_abi_double + int ok; + #else + error fail + #endif + ]])], + [float_abi=d], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __riscv_float_abi_single + int ok; + #else + error fail + #endif + ]])], + [float_abi=f], + [float_abi='']) + ]) + gl_cv_host_cpu_c_abi="${cpu}-${main_abi}${float_abi}" + ;; + + s390* ) + # On s390x, the C compiler may be generating 64-bit (= s390x) code + # or 31-bit (= s390) code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __LP64__ || defined __s390x__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=s390x], + [gl_cv_host_cpu_c_abi=s390]) + ;; + + sparc | sparc64 ) + # UltraSPARCs running Linux have `uname -m` = "sparc64", but the + # C compiler still generates 32-bit code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __sparcv9 || defined __arch64__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=sparc64], + [gl_cv_host_cpu_c_abi=sparc]) + ;; + + *) + gl_cv_host_cpu_c_abi="$host_cpu" + ;; + esac + ]) + + dnl In most cases, $HOST_CPU and $HOST_CPU_C_ABI are the same. + HOST_CPU=`echo "$gl_cv_host_cpu_c_abi" | sed -e 's/-.*//'` + HOST_CPU_C_ABI="$gl_cv_host_cpu_c_abi" + AC_SUBST([HOST_CPU]) + AC_SUBST([HOST_CPU_C_ABI]) + + # This was + # AC_DEFINE_UNQUOTED([__${HOST_CPU}__]) + # AC_DEFINE_UNQUOTED([__${HOST_CPU_C_ABI}__]) + # earlier, but KAI C++ 3.2d doesn't like this. + sed -e 's/-/_/g' >> confdefs.h <&1 /dev/null 2>&1 \ + && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \ + || PATH_SEPARATOR=';' + } +fi + +if test -n "$LD"; then + AC_MSG_CHECKING([for ld]) +elif test "$GCC" = yes; then + AC_MSG_CHECKING([for ld used by $CC]) +elif test "$with_gnu_ld" = yes; then + AC_MSG_CHECKING([for GNU ld]) +else + AC_MSG_CHECKING([for non-GNU ld]) +fi +if test -n "$LD"; then + # Let the user override the test with a path. + : +else + AC_CACHE_VAL([acl_cv_path_LD], + [ + acl_cv_path_LD= # Final result of this test + ac_prog=ld # Program to search in $PATH + if test "$GCC" = yes; then + # Check if gcc -print-prog-name=ld gives a path. + case $host in + *-*-mingw*) + # gcc leaves a trailing carriage return which upsets mingw + acl_output=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;; + *) + acl_output=`($CC -print-prog-name=ld) 2>&5` ;; + esac + case $acl_output in + # Accept absolute paths. + [[\\/]]* | ?:[[\\/]]*) + re_direlt='/[[^/]][[^/]]*/\.\./' + # Canonicalize the pathname of ld + acl_output=`echo "$acl_output" | sed 's%\\\\%/%g'` + while echo "$acl_output" | grep "$re_direlt" > /dev/null 2>&1; do + acl_output=`echo $acl_output | sed "s%$re_direlt%/%"` + done + # Got the pathname. No search in PATH is needed. + acl_cv_path_LD="$acl_output" + ac_prog= + ;; + "") + # If it fails, then pretend we aren't using GCC. + ;; + *) + # If it is relative, then search for the first ld in PATH. + with_gnu_ld=unknown + ;; + esac + fi + if test -n "$ac_prog"; then + # Search for $ac_prog in $PATH. + acl_save_ifs="$IFS"; IFS=$PATH_SEPARATOR + for ac_dir in $PATH; do + IFS="$acl_save_ifs" + test -z "$ac_dir" && ac_dir=. + if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then + acl_cv_path_LD="$ac_dir/$ac_prog" + # Check to see if the program is GNU ld. I'd rather use --version, + # but apparently some variants of GNU ld only accept -v. + # Break only if it was the GNU/non-GNU ld that we prefer. + case `"$acl_cv_path_LD" -v 2>&1 conftest.sh + . ./conftest.sh + rm -f ./conftest.sh + acl_cv_rpath=done + ]) + wl="$acl_cv_wl" + acl_libext="$acl_cv_libext" + acl_shlibext="$acl_cv_shlibext" + acl_libname_spec="$acl_cv_libname_spec" + acl_library_names_spec="$acl_cv_library_names_spec" + acl_hardcode_libdir_flag_spec="$acl_cv_hardcode_libdir_flag_spec" + acl_hardcode_libdir_separator="$acl_cv_hardcode_libdir_separator" + acl_hardcode_direct="$acl_cv_hardcode_direct" + acl_hardcode_minus_L="$acl_cv_hardcode_minus_L" + dnl Determine whether the user wants rpath handling at all. + AC_ARG_ENABLE([rpath], + [ --disable-rpath do not hardcode runtime library paths], + :, enable_rpath=yes) +]) + +dnl AC_LIB_FROMPACKAGE(name, package) +dnl declares that libname comes from the given package. The configure file +dnl will then not have a --with-libname-prefix option but a +dnl --with-package-prefix option. Several libraries can come from the same +dnl package. This declaration must occur before an AC_LIB_LINKFLAGS or similar +dnl macro call that searches for libname. +AC_DEFUN([AC_LIB_FROMPACKAGE], +[ + pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + define([acl_frompackage_]NAME, [$2]) + popdef([NAME]) + pushdef([PACK],[$2]) + pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + define([acl_libsinpackage_]PACKUP, + m4_ifdef([acl_libsinpackage_]PACKUP, [m4_defn([acl_libsinpackage_]PACKUP)[, ]],)[lib$1]) + popdef([PACKUP]) + popdef([PACK]) +]) + +dnl AC_LIB_LINKFLAGS_BODY(name [, dependencies]) searches for libname and +dnl the libraries corresponding to explicit and implicit dependencies. +dnl Sets the LIB${NAME}, LTLIB${NAME} and INC${NAME} variables. +dnl Also, sets the LIB${NAME}_PREFIX variable to nonempty if libname was found +dnl in ${LIB${NAME}_PREFIX}/$acl_libdirstem. +AC_DEFUN([AC_LIB_LINKFLAGS_BODY], +[ + AC_REQUIRE([AC_LIB_PREPARE_MULTILIB]) + pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + pushdef([PACK],[m4_ifdef([acl_frompackage_]NAME, [acl_frompackage_]NAME, lib[$1])]) + pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + pushdef([PACKLIBS],[m4_ifdef([acl_frompackage_]NAME, [acl_libsinpackage_]PACKUP, lib[$1])]) + dnl By default, look in $includedir and $libdir. + use_additional=yes + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + AC_ARG_WITH(PACK[-prefix], +[[ --with-]]PACK[[-prefix[=DIR] search for ]PACKLIBS[ in DIR/include and DIR/lib + --without-]]PACK[[-prefix don't search for ]PACKLIBS[ in includedir and libdir]], +[ + if test "X$withval" = "Xno"; then + use_additional=no + else + if test "X$withval" = "X"; then + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + else + additional_includedir="$withval/include" + additional_libdir="$withval/$acl_libdirstem" + if test "$acl_libdirstem2" != "$acl_libdirstem" \ + && test ! -d "$withval/$acl_libdirstem"; then + additional_libdir="$withval/$acl_libdirstem2" + fi + fi + fi +]) + dnl Search the library and its dependencies in $additional_libdir and + dnl $LDFLAGS. Using breadth-first-seach. + LIB[]NAME= + LTLIB[]NAME= + INC[]NAME= + LIB[]NAME[]_PREFIX= + dnl HAVE_LIB${NAME} is an indicator that LIB${NAME}, LTLIB${NAME} have been + dnl computed. So it has to be reset here. + HAVE_LIB[]NAME= + rpathdirs= + ltrpathdirs= + names_already_handled= + names_next_round='$1 $2' + while test -n "$names_next_round"; do + names_this_round="$names_next_round" + names_next_round= + for name in $names_this_round; do + already_handled= + for n in $names_already_handled; do + if test "$n" = "$name"; then + already_handled=yes + break + fi + done + if test -z "$already_handled"; then + names_already_handled="$names_already_handled $name" + dnl See if it was already located by an earlier AC_LIB_LINKFLAGS + dnl or AC_LIB_HAVE_LINKFLAGS call. + uppername=`echo "$name" | sed -e 'y|abcdefghijklmnopqrstuvwxyz./+-|ABCDEFGHIJKLMNOPQRSTUVWXYZ____|'` + eval value=\"\$HAVE_LIB$uppername\" + if test -n "$value"; then + if test "$value" = yes; then + eval value=\"\$LIB$uppername\" + test -z "$value" || LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$value" + eval value=\"\$LTLIB$uppername\" + test -z "$value" || LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$value" + else + dnl An earlier call to AC_LIB_HAVE_LINKFLAGS has determined + dnl that this library doesn't exist. So just drop it. + : + fi + else + dnl Search the library lib$name in $additional_libdir and $LDFLAGS + dnl and the already constructed $LIBNAME/$LTLIBNAME. + found_dir= + found_la= + found_so= + found_a= + eval libname=\"$acl_libname_spec\" # typically: libname=lib$name + if test -n "$acl_shlibext"; then + shrext=".$acl_shlibext" # typically: shrext=.so + else + shrext= + fi + if test $use_additional = yes; then + dir="$additional_libdir" + dnl The same code as in the loop below: + dnl First look for a shared library. + if test -n "$acl_shlibext"; then + if test -f "$dir/$libname$shrext"; then + found_dir="$dir" + found_so="$dir/$libname$shrext" + else + if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then + ver=`(cd "$dir" && \ + for f in "$libname$shrext".*; do echo "$f"; done \ + | sed -e "s,^$libname$shrext\\\\.,," \ + | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \ + | sed 1q ) 2>/dev/null` + if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then + found_dir="$dir" + found_so="$dir/$libname$shrext.$ver" + fi + else + eval library_names=\"$acl_library_names_spec\" + for f in $library_names; do + if test -f "$dir/$f"; then + found_dir="$dir" + found_so="$dir/$f" + break + fi + done + fi + fi + fi + dnl Then look for a static library. + if test "X$found_dir" = "X"; then + if test -f "$dir/$libname.$acl_libext"; then + found_dir="$dir" + found_a="$dir/$libname.$acl_libext" + fi + fi + if test "X$found_dir" != "X"; then + if test -f "$dir/$libname.la"; then + found_la="$dir/$libname.la" + fi + fi + fi + if test "X$found_dir" = "X"; then + for x in $LDFLAGS $LTLIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + case "$x" in + -L*) + dir=`echo "X$x" | sed -e 's/^X-L//'` + dnl First look for a shared library. + if test -n "$acl_shlibext"; then + if test -f "$dir/$libname$shrext"; then + found_dir="$dir" + found_so="$dir/$libname$shrext" + else + if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then + ver=`(cd "$dir" && \ + for f in "$libname$shrext".*; do echo "$f"; done \ + | sed -e "s,^$libname$shrext\\\\.,," \ + | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \ + | sed 1q ) 2>/dev/null` + if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then + found_dir="$dir" + found_so="$dir/$libname$shrext.$ver" + fi + else + eval library_names=\"$acl_library_names_spec\" + for f in $library_names; do + if test -f "$dir/$f"; then + found_dir="$dir" + found_so="$dir/$f" + break + fi + done + fi + fi + fi + dnl Then look for a static library. + if test "X$found_dir" = "X"; then + if test -f "$dir/$libname.$acl_libext"; then + found_dir="$dir" + found_a="$dir/$libname.$acl_libext" + fi + fi + if test "X$found_dir" != "X"; then + if test -f "$dir/$libname.la"; then + found_la="$dir/$libname.la" + fi + fi + ;; + esac + if test "X$found_dir" != "X"; then + break + fi + done + fi + if test "X$found_dir" != "X"; then + dnl Found the library. + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$found_dir -l$name" + if test "X$found_so" != "X"; then + dnl Linking with a shared library. We attempt to hardcode its + dnl directory into the executable's runpath, unless it's the + dnl standard /usr/lib. + if test "$enable_rpath" = no \ + || test "X$found_dir" = "X/usr/$acl_libdirstem" \ + || test "X$found_dir" = "X/usr/$acl_libdirstem2"; then + dnl No hardcoding is needed. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + else + dnl Use an explicit option to hardcode DIR into the resulting + dnl binary. + dnl Potentially add DIR to ltrpathdirs. + dnl The ltrpathdirs will be appended to $LTLIBNAME at the end. + haveit= + for x in $ltrpathdirs; do + if test "X$x" = "X$found_dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + ltrpathdirs="$ltrpathdirs $found_dir" + fi + dnl The hardcoding into $LIBNAME is system dependent. + if test "$acl_hardcode_direct" = yes; then + dnl Using DIR/libNAME.so during linking hardcodes DIR into the + dnl resulting binary. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + else + if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then + dnl Use an explicit option to hardcode DIR into the resulting + dnl binary. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + dnl Potentially add DIR to rpathdirs. + dnl The rpathdirs will be appended to $LIBNAME at the end. + haveit= + for x in $rpathdirs; do + if test "X$x" = "X$found_dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + rpathdirs="$rpathdirs $found_dir" + fi + else + dnl Rely on "-L$found_dir". + dnl But don't add it if it's already contained in the LDFLAGS + dnl or the already constructed $LIBNAME + haveit= + for x in $LDFLAGS $LIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$found_dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir" + fi + if test "$acl_hardcode_minus_L" != no; then + dnl FIXME: Not sure whether we should use + dnl "-L$found_dir -l$name" or "-L$found_dir $found_so" + dnl here. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + else + dnl We cannot use $acl_hardcode_runpath_var and LD_RUN_PATH + dnl here, because this doesn't fit in flags passed to the + dnl compiler. So give up. No hardcoding. This affects only + dnl very old systems. + dnl FIXME: Not sure whether we should use + dnl "-L$found_dir -l$name" or "-L$found_dir $found_so" + dnl here. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name" + fi + fi + fi + fi + else + if test "X$found_a" != "X"; then + dnl Linking with a static library. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_a" + else + dnl We shouldn't come here, but anyway it's good to have a + dnl fallback. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir -l$name" + fi + fi + dnl Assume the include files are nearby. + additional_includedir= + case "$found_dir" in + */$acl_libdirstem | */$acl_libdirstem/) + basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem/"'*$,,'` + if test "$name" = '$1'; then + LIB[]NAME[]_PREFIX="$basedir" + fi + additional_includedir="$basedir/include" + ;; + */$acl_libdirstem2 | */$acl_libdirstem2/) + basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem2/"'*$,,'` + if test "$name" = '$1'; then + LIB[]NAME[]_PREFIX="$basedir" + fi + additional_includedir="$basedir/include" + ;; + esac + if test "X$additional_includedir" != "X"; then + dnl Potentially add $additional_includedir to $INCNAME. + dnl But don't add it + dnl 1. if it's the standard /usr/include, + dnl 2. if it's /usr/local/include and we are using GCC on Linux, + dnl 3. if it's already present in $CPPFLAGS or the already + dnl constructed $INCNAME, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_includedir" != "X/usr/include"; then + haveit= + if test "X$additional_includedir" = "X/usr/local/include"; then + if test -n "$GCC"; then + case $host_os in + linux* | gnu* | k*bsd*-gnu) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + for x in $CPPFLAGS $INC[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-I$additional_includedir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test -d "$additional_includedir"; then + dnl Really add $additional_includedir to $INCNAME. + INC[]NAME="${INC[]NAME}${INC[]NAME:+ }-I$additional_includedir" + fi + fi + fi + fi + fi + dnl Look for dependencies. + if test -n "$found_la"; then + dnl Read the .la file. It defines the variables + dnl dlname, library_names, old_library, dependency_libs, current, + dnl age, revision, installed, dlopen, dlpreopen, libdir. + save_libdir="$libdir" + case "$found_la" in + */* | *\\*) . "$found_la" ;; + *) . "./$found_la" ;; + esac + libdir="$save_libdir" + dnl We use only dependency_libs. + for dep in $dependency_libs; do + case "$dep" in + -L*) + additional_libdir=`echo "X$dep" | sed -e 's/^X-L//'` + dnl Potentially add $additional_libdir to $LIBNAME and $LTLIBNAME. + dnl But don't add it + dnl 1. if it's the standard /usr/lib, + dnl 2. if it's /usr/local/lib and we are using GCC on Linux, + dnl 3. if it's already present in $LDFLAGS or the already + dnl constructed $LIBNAME, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_libdir" != "X/usr/$acl_libdirstem" \ + && test "X$additional_libdir" != "X/usr/$acl_libdirstem2"; then + haveit= + if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem" \ + || test "X$additional_libdir" = "X/usr/local/$acl_libdirstem2"; then + if test -n "$GCC"; then + case $host_os in + linux* | gnu* | k*bsd*-gnu) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + haveit= + for x in $LDFLAGS $LIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$additional_libdir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test -d "$additional_libdir"; then + dnl Really add $additional_libdir to $LIBNAME. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$additional_libdir" + fi + fi + haveit= + for x in $LDFLAGS $LTLIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$additional_libdir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test -d "$additional_libdir"; then + dnl Really add $additional_libdir to $LTLIBNAME. + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$additional_libdir" + fi + fi + fi + fi + ;; + -R*) + dir=`echo "X$dep" | sed -e 's/^X-R//'` + if test "$enable_rpath" != no; then + dnl Potentially add DIR to rpathdirs. + dnl The rpathdirs will be appended to $LIBNAME at the end. + haveit= + for x in $rpathdirs; do + if test "X$x" = "X$dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + rpathdirs="$rpathdirs $dir" + fi + dnl Potentially add DIR to ltrpathdirs. + dnl The ltrpathdirs will be appended to $LTLIBNAME at the end. + haveit= + for x in $ltrpathdirs; do + if test "X$x" = "X$dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + ltrpathdirs="$ltrpathdirs $dir" + fi + fi + ;; + -l*) + dnl Handle this in the next round. + names_next_round="$names_next_round "`echo "X$dep" | sed -e 's/^X-l//'` + ;; + *.la) + dnl Handle this in the next round. Throw away the .la's + dnl directory; it is already contained in a preceding -L + dnl option. + names_next_round="$names_next_round "`echo "X$dep" | sed -e 's,^X.*/,,' -e 's,^lib,,' -e 's,\.la$,,'` + ;; + *) + dnl Most likely an immediate library name. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$dep" + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$dep" + ;; + esac + done + fi + else + dnl Didn't find the library; assume it is in the system directories + dnl known to the linker and runtime loader. (All the system + dnl directories known to the linker should also be known to the + dnl runtime loader, otherwise the system is severely misconfigured.) + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name" + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-l$name" + fi + fi + fi + done + done + if test "X$rpathdirs" != "X"; then + if test -n "$acl_hardcode_libdir_separator"; then + dnl Weird platform: only the last -rpath option counts, the user must + dnl pass all path elements in one option. We can arrange that for a + dnl single library, but not when more than one $LIBNAMEs are used. + alldirs= + for found_dir in $rpathdirs; do + alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$found_dir" + done + dnl Note: acl_hardcode_libdir_flag_spec uses $libdir and $wl. + acl_save_libdir="$libdir" + libdir="$alldirs" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag" + else + dnl The -rpath options are cumulative. + for found_dir in $rpathdirs; do + acl_save_libdir="$libdir" + libdir="$found_dir" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag" + done + fi + fi + if test "X$ltrpathdirs" != "X"; then + dnl When using libtool, the option that works for both libraries and + dnl executables is -R. The -R options are cumulative. + for found_dir in $ltrpathdirs; do + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-R$found_dir" + done + fi + popdef([PACKLIBS]) + popdef([PACKUP]) + popdef([PACK]) + popdef([NAME]) +]) + +dnl AC_LIB_APPENDTOVAR(VAR, CONTENTS) appends the elements of CONTENTS to VAR, +dnl unless already present in VAR. +dnl Works only for CPPFLAGS, not for LIB* variables because that sometimes +dnl contains two or three consecutive elements that belong together. +AC_DEFUN([AC_LIB_APPENDTOVAR], +[ + for element in [$2]; do + haveit= + for x in $[$1]; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X$element"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + [$1]="${[$1]}${[$1]:+ }$element" + fi + done +]) + +dnl For those cases where a variable contains several -L and -l options +dnl referring to unknown libraries and directories, this macro determines the +dnl necessary additional linker options for the runtime path. +dnl AC_LIB_LINKFLAGS_FROM_LIBS([LDADDVAR], [LIBSVALUE], [USE-LIBTOOL]) +dnl sets LDADDVAR to linker options needed together with LIBSVALUE. +dnl If USE-LIBTOOL evaluates to non-empty, linking with libtool is assumed, +dnl otherwise linking without libtool is assumed. +AC_DEFUN([AC_LIB_LINKFLAGS_FROM_LIBS], +[ + AC_REQUIRE([AC_LIB_RPATH]) + AC_REQUIRE([AC_LIB_PREPARE_MULTILIB]) + $1= + if test "$enable_rpath" != no; then + if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then + dnl Use an explicit option to hardcode directories into the resulting + dnl binary. + rpathdirs= + next= + for opt in $2; do + if test -n "$next"; then + dir="$next" + dnl No need to hardcode the standard /usr/lib. + if test "X$dir" != "X/usr/$acl_libdirstem" \ + && test "X$dir" != "X/usr/$acl_libdirstem2"; then + rpathdirs="$rpathdirs $dir" + fi + next= + else + case $opt in + -L) next=yes ;; + -L*) dir=`echo "X$opt" | sed -e 's,^X-L,,'` + dnl No need to hardcode the standard /usr/lib. + if test "X$dir" != "X/usr/$acl_libdirstem" \ + && test "X$dir" != "X/usr/$acl_libdirstem2"; then + rpathdirs="$rpathdirs $dir" + fi + next= ;; + *) next= ;; + esac + fi + done + if test "X$rpathdirs" != "X"; then + if test -n ""$3""; then + dnl libtool is used for linking. Use -R options. + for dir in $rpathdirs; do + $1="${$1}${$1:+ }-R$dir" + done + else + dnl The linker is used for linking directly. + if test -n "$acl_hardcode_libdir_separator"; then + dnl Weird platform: only the last -rpath option counts, the user + dnl must pass all path elements in one option. + alldirs= + for dir in $rpathdirs; do + alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$dir" + done + acl_save_libdir="$libdir" + libdir="$alldirs" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + $1="$flag" + else + dnl The -rpath options are cumulative. + for dir in $rpathdirs; do + acl_save_libdir="$libdir" + libdir="$dir" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + $1="${$1}${$1:+ }$flag" + done + fi + fi + fi + fi + fi + AC_SUBST([$1]) +]) diff --git a/m4/lib-prefix.m4 b/m4/lib-prefix.m4 new file mode 100644 index 0000000..8adb17b --- /dev/null +++ b/m4/lib-prefix.m4 @@ -0,0 +1,249 @@ +# lib-prefix.m4 serial 14 +dnl Copyright (C) 2001-2005, 2008-2019 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Bruno Haible. + +dnl AC_LIB_PREFIX adds to the CPPFLAGS and LDFLAGS the flags that are needed +dnl to access previously installed libraries. The basic assumption is that +dnl a user will want packages to use other packages he previously installed +dnl with the same --prefix option. +dnl This macro is not needed if only AC_LIB_LINKFLAGS is used to locate +dnl libraries, but is otherwise very convenient. +AC_DEFUN([AC_LIB_PREFIX], +[ + AC_BEFORE([$0], [AC_LIB_LINKFLAGS]) + AC_REQUIRE([AC_PROG_CC]) + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([AC_LIB_PREPARE_MULTILIB]) + AC_REQUIRE([AC_LIB_PREPARE_PREFIX]) + dnl By default, look in $includedir and $libdir. + use_additional=yes + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + AC_ARG_WITH([lib-prefix], +[[ --with-lib-prefix[=DIR] search for libraries in DIR/include and DIR/lib + --without-lib-prefix don't search for libraries in includedir and libdir]], +[ + if test "X$withval" = "Xno"; then + use_additional=no + else + if test "X$withval" = "X"; then + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + else + additional_includedir="$withval/include" + additional_libdir="$withval/$acl_libdirstem" + fi + fi +]) + if test $use_additional = yes; then + dnl Potentially add $additional_includedir to $CPPFLAGS. + dnl But don't add it + dnl 1. if it's the standard /usr/include, + dnl 2. if it's already present in $CPPFLAGS, + dnl 3. if it's /usr/local/include and we are using GCC on Linux, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_includedir" != "X/usr/include"; then + haveit= + for x in $CPPFLAGS; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-I$additional_includedir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test "X$additional_includedir" = "X/usr/local/include"; then + if test -n "$GCC"; then + case $host_os in + linux* | gnu* | k*bsd*-gnu) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + if test -d "$additional_includedir"; then + dnl Really add $additional_includedir to $CPPFLAGS. + CPPFLAGS="${CPPFLAGS}${CPPFLAGS:+ }-I$additional_includedir" + fi + fi + fi + fi + dnl Potentially add $additional_libdir to $LDFLAGS. + dnl But don't add it + dnl 1. if it's the standard /usr/lib, + dnl 2. if it's already present in $LDFLAGS, + dnl 3. if it's /usr/local/lib and we are using GCC on Linux, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_libdir" != "X/usr/$acl_libdirstem"; then + haveit= + for x in $LDFLAGS; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$additional_libdir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem"; then + if test -n "$GCC"; then + case $host_os in + linux*) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + if test -d "$additional_libdir"; then + dnl Really add $additional_libdir to $LDFLAGS. + LDFLAGS="${LDFLAGS}${LDFLAGS:+ }-L$additional_libdir" + fi + fi + fi + fi + fi +]) + +dnl AC_LIB_PREPARE_PREFIX creates variables acl_final_prefix, +dnl acl_final_exec_prefix, containing the values to which $prefix and +dnl $exec_prefix will expand at the end of the configure script. +AC_DEFUN([AC_LIB_PREPARE_PREFIX], +[ + dnl Unfortunately, prefix and exec_prefix get only finally determined + dnl at the end of configure. + if test "X$prefix" = "XNONE"; then + acl_final_prefix="$ac_default_prefix" + else + acl_final_prefix="$prefix" + fi + if test "X$exec_prefix" = "XNONE"; then + acl_final_exec_prefix='${prefix}' + else + acl_final_exec_prefix="$exec_prefix" + fi + acl_save_prefix="$prefix" + prefix="$acl_final_prefix" + eval acl_final_exec_prefix=\"$acl_final_exec_prefix\" + prefix="$acl_save_prefix" +]) + +dnl AC_LIB_WITH_FINAL_PREFIX([statement]) evaluates statement, with the +dnl variables prefix and exec_prefix bound to the values they will have +dnl at the end of the configure script. +AC_DEFUN([AC_LIB_WITH_FINAL_PREFIX], +[ + acl_save_prefix="$prefix" + prefix="$acl_final_prefix" + acl_save_exec_prefix="$exec_prefix" + exec_prefix="$acl_final_exec_prefix" + $1 + exec_prefix="$acl_save_exec_prefix" + prefix="$acl_save_prefix" +]) + +dnl AC_LIB_PREPARE_MULTILIB creates +dnl - a variable acl_libdirstem, containing the basename of the libdir, either +dnl "lib" or "lib64" or "lib/64", +dnl - a variable acl_libdirstem2, as a secondary possible value for +dnl acl_libdirstem, either the same as acl_libdirstem or "lib/sparcv9" or +dnl "lib/amd64". +AC_DEFUN([AC_LIB_PREPARE_MULTILIB], +[ + dnl There is no formal standard regarding lib and lib64. + dnl On glibc systems, the current practice is that on a system supporting + dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under + dnl $prefix/lib64 and 32-bit libraries go under $prefix/lib. We determine + dnl the compiler's default mode by looking at the compiler's library search + dnl path. If at least one of its elements ends in /lib64 or points to a + dnl directory whose absolute pathname ends in /lib64, we assume a 64-bit ABI. + dnl Otherwise we use the default, namely "lib". + dnl On Solaris systems, the current practice is that on a system supporting + dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under + dnl $prefix/lib/64 (which is a symlink to either $prefix/lib/sparcv9 or + dnl $prefix/lib/amd64) and 32-bit libraries go under $prefix/lib. + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([gl_HOST_CPU_C_ABI_32BIT]) + + case "$host_os" in + solaris*) + AC_CACHE_CHECK([for 64-bit host], [gl_cv_solaris_64bit], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef _LP64 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_solaris_64bit=yes], + [gl_cv_solaris_64bit=no]) + ]);; + esac + + dnl Allow the user to override the result by setting acl_cv_libdirstems. + AC_CACHE_CHECK([for the common suffixes of directories in the library search path], + [acl_cv_libdirstems], + [acl_libdirstem=lib + acl_libdirstem2= + case "$host_os" in + solaris*) + dnl See Solaris 10 Software Developer Collection > Solaris 64-bit Developer's Guide > The Development Environment + dnl . + dnl "Portable Makefiles should refer to any library directories using the 64 symbolic link." + dnl But we want to recognize the sparcv9 or amd64 subdirectory also if the + dnl symlink is missing, so we set acl_libdirstem2 too. + if test $gl_cv_solaris_64bit = yes; then + acl_libdirstem=lib/64 + case "$host_cpu" in + sparc*) acl_libdirstem2=lib/sparcv9 ;; + i*86 | x86_64) acl_libdirstem2=lib/amd64 ;; + esac + fi + ;; + *) + dnl If $CC generates code for a 32-bit ABI, the libraries are + dnl surely under $prefix/lib, not $prefix/lib64. + if test "$HOST_CPU_C_ABI_32BIT" != yes; then + dnl The result is a property of the system. However, non-system + dnl compilers sometimes have odd library search paths. Therefore + dnl prefer asking /usr/bin/gcc, if available, rather than $CC. + searchpath=`(if test -f /usr/bin/gcc \ + && LC_ALL=C /usr/bin/gcc -print-search-dirs >/dev/null 2>/dev/null; then \ + LC_ALL=C /usr/bin/gcc -print-search-dirs; \ + else \ + LC_ALL=C $CC -print-search-dirs; \ + fi) 2>/dev/null \ + | sed -n -e 's,^libraries: ,,p' | sed -e 's,^=,,'` + if test -n "$searchpath"; then + acl_save_IFS="${IFS= }"; IFS=":" + for searchdir in $searchpath; do + if test -d "$searchdir"; then + case "$searchdir" in + */lib64/ | */lib64 ) acl_libdirstem=lib64 ;; + */../ | */.. ) + # Better ignore directories of this form. They are misleading. + ;; + *) searchdir=`cd "$searchdir" && pwd` + case "$searchdir" in + */lib64 ) acl_libdirstem=lib64 ;; + esac ;; + esac + fi + done + IFS="$acl_save_IFS" + fi + fi + ;; + esac + test -n "$acl_libdirstem2" || acl_libdirstem2="$acl_libdirstem" + acl_cv_libdirstems="$acl_libdirstem,$acl_libdirstem2" + ]) + # Decompose acl_cv_libdirstems into acl_libdirstem and acl_libdirstem2. + acl_libdirstem=`echo "$acl_cv_libdirstems" | sed -e 's/,.*//'` + acl_libdirstem2=`echo "$acl_cv_libdirstems" | sed -e '/,/s/.*,//'` +]) diff --git a/man/Makefile.am b/man/Makefile.am new file mode 100644 index 0000000..ac7fb93 --- /dev/null +++ b/man/Makefile.am @@ -0,0 +1,38 @@ +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +dist_man_MANS = \ + mu-add.1 \ + mu-bookmarks.5 \ + mu-cfind.1 \ + mu-easy.1 \ + mu-extract.1 \ + mu-fields.1 \ + mu-find.1 \ + mu-help.1 \ + mu-index.1 \ + mu-info.1 \ + mu-init.1 \ + mu-mkdir.1 \ + mu-query.7 \ + mu-remove.1 \ + mu-server.1 \ + mu-script.1 \ + mu-verify.1 \ + mu-view.1 \ + mu.1 diff --git a/man/meson.build b/man/meson.build new file mode 100644 index 0000000..81d7e4c --- /dev/null +++ b/man/meson.build @@ -0,0 +1,36 @@ +## Copyright (C) 2021 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +install_man( + ['mu.1', + 'mu-add.1', + 'mu-bookmarks.5', + 'mu-cfind.1', + 'mu-easy.1', + 'mu-extract.1', + 'mu-fields.1', + 'mu-find.1', + 'mu-help.1', + 'mu-index.1', + 'mu-info.1', + 'mu-init.1', + 'mu-mkdir.1', + 'mu-query.7', + 'mu-remove.1', + 'mu-script.1', + 'mu-server.1', + 'mu-verify.1', + 'mu-view.1']) diff --git a/man/mu-add.1 b/man/mu-add.1 new file mode 100644 index 0000000..1bf1ed9 --- /dev/null +++ b/man/mu-add.1 @@ -0,0 +1,47 @@ +.TH MU ADD 1 "May 2022" "User Manuals" + +.SH NAME + +mu add\- add one or more messages to the database + +.SH SYNOPSIS + +.B mu add [] + +.SH DESCRIPTION + +\fBmu add\fR is the command to add specific message files to the +database. Each file must be specified with an absolute path. + +.SH OPTIONS + +\fBmu add\fR does not have its own options, but the general options for +determining the location of the database (\fI--muhome\fR) are available. See +\fBmu-index\fR(1) for more information. + +.SH RETURN VALUE + +\fBmu add\fR returns 0 upon success; in general, the following error codes are +returned: + +.nf +| code | meaning | +|------+-----------------------------------| +| 0 | ok | +| 1 | general error | +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-remove (1) diff --git a/man/mu-bookmarks.5 b/man/mu-bookmarks.5 new file mode 100644 index 0000000..f68a335 --- /dev/null +++ b/man/mu-bookmarks.5 @@ -0,0 +1,36 @@ +.TH MU-BOOKMARKS 5 "July 2019" "User Manuals" + +.SH NAME + +bookmarks \- file with bookmarks (shortcuts) for mu search expressions + +.SH DESCRIPTION + +Bookmarks are named shortcuts for search queries. They allow using a convenient +name for often-used queries. The bookmarks are also visible as shortcuts in the +mu experimental user interfaces, \fImug\fR and \fImug2\fR. + +The bookmarks file is read from \fI/bookmarks\fR. On Unix this would +typically be w be \fI~/.config/mu/bookmarks\fR, but this can be influenced using +the \fB\-\-muhome\fR parameter for \fBmu-find\fR(1) and \fBmug\fR(1). + +The bookmarks file is a typical key=value \fB.ini\fR-file, which is best shown +by means of an example: + +.nf + [mu] + inbox=maildir:/inbox # inbox + oldhat=maildir:/archive subject:hat # archived with subject containing 'hat' +.fi + +The \fB[mu]\fR group header is required. + +For practical uses of bookmarks, see \fBmu-find\fR(1). + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), mu-find (1) diff --git a/man/mu-cfind.1 b/man/mu-cfind.1 new file mode 100644 index 0000000..a3618e1 --- /dev/null +++ b/man/mu-cfind.1 @@ -0,0 +1,133 @@ +.TH MU CFIND 1 "April 2019" "User Manuals" + +.SH NAME + +\fBmu cfind\fR is the \fBmu\fR command to find contacts in the \fBmu\fR +database and export them for use in other programs. + +.SH SYNOPSIS + +.B mu cfind [options] [] + +.SH DESCRIPTION + +\fBmu cfind\fR is the \fBmu\fR command for finding \fIcontacts\fR (name and +e-mail address of people who were either an e-mail's sender or +receiver). There are different output formats available, for importing the +contacts into other programs. + +.SH SEARCHING CONTACTS + +When you index your messages (see \fBmu index\fR), \fBmu\fR creates a list of +unique e-mail addresses found and the accompanying name, and caches this +list. In case the same e-mail address is used with different names, the most +recent non-empty name is used. + +\fBmu cfind\fR starts a search for contacts that match a \fIregular +expression\fR. For example: + +.nf + $ mu cfind '@gmail\.com' +.fi + +would find all contacts with a gmail-address, while + +.nf + $ mu cfind Mary +.fi + +lists all contacts with Mary in either name or e-mail address. + +If you do not specify a search expression, \fBmu cfind\fR returns the full list +of contacts. Note, \fBmu cfind\fR uses a cache with the e-mail information, +which is populated during the indexing process. + +The regular expressions are Perl-compatible (as per the PCRE-library used by +GRegex). + +.SH OPTIONS + +.TP +\fB\-\-format\fR=\fIplain|mutt-alias|mutt-ab|wl|org-contact|bbdb|csv\fR +sets the output format to the given value. The following are available: + +.nf +| --format= | description | +|-------------+-----------------------------------| +| plain | default, simple list | +| mutt-alias | mutt alias-format | +| mutt-ab | mutt external address book format | +| wl | wanderlust addressbook format | +| org-contact | org-mode org-contact format | +| bbdb | BBDB format | +| csv | comma-separated values (*) | +.fi + + +(*) CSV is not fully standardized, but \fBmu cfind\fR follows some common +practices: any double-quote is replaced by a double-double quote (thus, "hello" +become ""hello"", and fields with commas are put in double-quotes. Normally, +this should only apply to name fields. + +.TP +\fB\-\-personal\fR only show addresses seen in messages where one of 'my' e-mail +addresses was seen in one of the address fields; this is to exclude addresses +only seen in mailing-list messages. See the \fB\-\-my-address\fR parameter in +\fBmu index\fR. + +.TP +\fB\-\-after=\fR\fI\fR only show addresses last seen after +\fI\fR. \fI\fR is a UNIX \fBtime_t\fR value, the number of +seconds since 1970-01-01 (in UTC). + +From the command line, you can use the \fBdate\fR command to get this value. For +example, only consider addresses last seen after 2009-06-01, you could specify +.nf + --after=`date +%s --date='2009-06-01'` +.fi + +.SH RETURN VALUE + +\fBmu cfind\fR returns 0 upon successful completion -- that is, at least one +contact was found. Anything else leads to a non-zero return value: + +.nf +| code | meaning | +|------+--------------------------------| +| 0 | ok | +| 1 | general error | +| 2 | no matches (for 'mu cfind') | +.fi + +.SH INTEGRATION WITH MUTT + +You can use \fBmu cfind\fR as an external address book server for \fBmutt\fR. +For this to work, add the following to your \fImuttrc\fR: + +.nf +set query_command = "mu cfind --format=mutt-ab '%s'" +.fi + +Now, in mutt, you can search for e-mail addresses using the \fBquery\fR-command, +which is (by default) accessible by pressing \fBQ\fR. + +.SH ENCODING + +\fBmu cfind\fR output is encoded according to the current locale except for +\fI--format=bbdb\fR. This is hard-coded to UTF-8, and as such specified in the +output-file, so emacs/bbdb can handle things correctly, without guessing. + +.SH BUGS + +Please report bugs if you find them at \fBhttps://github.com/djcb/mu/issues\fR. + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-find (1), +.BR pcrepattern(3) diff --git a/man/mu-easy.1 b/man/mu-easy.1 new file mode 100644 index 0000000..27e9258 --- /dev/null +++ b/man/mu-easy.1 @@ -0,0 +1,313 @@ +.TH MU-EASY 1 "February 2020" "User Manuals" + +.SH NAME + +mu easy \- a quick introduction to mu + +.SH DESCRIPTION + +\fBmu\fR is a set of tools for dealing with e-mail messages in Maildirs. There +are many options, which are all described in the man pages for the various +sub-commands. This man pages jumps over all of the details and gives examples of +some common use cases. If the use cases described here do not precisely do what +you want, please check the more extensive information in the man page about the +sub-command you are using -- for example, the \fBmu-index\fR(1) or +\fBmu-find\fR(1) man pages. + +\fBNOTE\fR: the \fBindex\fR command (and therefore, the ones that depend on +that, such as \fBfind\fR), require that you store your mail in the +Maildir-format. If you don't do so, you can still use the other commands, but +you won't be able to index/search your mail. + +By default, \fBmu\fR uses colorized output when it thinks your terminal is +capable of doing so. If you don't like color, you can use the \fB--nocolor\fR +command-line option, or set either the \fBMU_NOCOLOR\fR or the \fBNO_COLOR\fR +environment variable to non-empty. + +.SH SETTING THINGS UP + +The first time you run the mu commands, you need to initialize it. This is done +with the \fBinit\fR command. + +.nf + \fB$ mu init\fR +.fi + +This uses the defaults (see \fBmu-init(1)\fR for details on how to change that). + + +.SH INDEXING YOUR E-MAIL + +Before you can search e-mails, you'll first need to index them: + +.nf + \fB$ mu index\fR +.fi + +The process can take a few minutes, depending on the amount of mail +you have, the speed of your computer, hard drive etc. Usually, +indexing should be able to reach a speed of a few hundred messages per +second. + +\fBmu index\fR guesses the top-level Maildir to do its job; if it guesses wrong, +you can use the \fI--maildir\fR option to specify the top-level directory that +should be processed. See the \fBmu-index\fR(1) man page for more details. + +Normally, \fBmu index\fR visits all the directories under the top-level Maildir; +however, you can exclude certain directories (say, the 'trash' or 'spam' +folders) by creating a file called \fI.noindex\fR in the directory. When +\fBmu\fR sees such a file, it will exclude this directory and its +sub-directories from indexing. Also see \fB.noupdate\fR in the \fBmu-index\fR(1) +manpage. + +.SH SEARCHING YOUR E-MAIL + +After you have indexed your mail, you can start searching it. By default, the +search results are printed on standard output. Alternatively, the output can +take the form of Maildir with symbolic links to the found messages. This enables +integration with e-mail clients; see the \fBmu-find\fR(1) man page for details, +the syntax of the search parameters and so on. Here, we just give some examples +for common cases. + +You can use the \fBmu fields\fR command to get information about all possible +fields and flags. + +First, let's search for all messages sent to Julius (Caesar) regarding +fruit: + +.nf +\fB$ mu find t:julius fruit\fR +.fi + +This should return something like: + +.nf + 2008-07-31T21:57:25 EEST John Milton Fere libenter homines id quod volunt credunt +.fi + +This means there is a message to 'julius' with 'fruit' somewhere in the message. +In this case, it's a message from John Milton. Note that the date format depends +on your the language/locale you are using. + +How do we know that the message was sent to Julius Caesar? Well, it's not +visible from the results above, because the default fields that are shown are +date/sender/subject. However, we can change this using the \fI--fields\fR +parameter (try \fBmu fields\fR to see all the details): + +.nf + \fB$ mu find --fields="t s" t:julius fruit\fR +.fi + +In other words, display the 'To:'-field (t) and the subject (s). This should +return something like: +.nf + Julius Caesar Fere libenter homines id quod volunt credunt +.fi + +This is the same message found before, only with some different fields +displayed. + +By default, \fBmu\fR uses the logical AND for the search parameters -- that +is, it displays messages that match all the parameters. However, we can use +logical OR as well: + +.nf + \fB$ mu find t:julius OR f:socrates\fR +.fi + +In other words, display messages that are either sent to Julius Caesar +\fBor\fR are from Socrates. This could return something like: + +.nf + 2008-07-31T21:57:25 EEST Socrates cool stuff + 2008-07-31T21:57:25 EEST John Milton Fere libenter homines id quod volunt credunt +.fi + +What if we want to see some of the body of the message? You can get +a 'summary' of the first lines of the message using the \fI--summary-len\fR +option, which will 'summarize' the first \fIn\fR lines of the message: + +.nf + \fB$ mu find --summary-len=3 napoleon m:/archive\fR +.fi + +.nf + 1970-01-01T02:00:00 EET Napoleon Bonaparte rock on dude + Summary: Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le + trois-mâts le Pharaon, venant de Smyrne, Trieste et Naples. Comme + d'habitude, un pilote côtier partit aussitôt du port, rasa le château +.fi + +The summary consists of the first n lines of the message with all superfluous +whitespace removed. + +Also note the \fBm:/archive\fR parameter in the query. This means that we only +match messages in a maildir called '/archive'. + +.SH MORE QUERIES + +Let's list a few more queries that may be interesting; please note that +searches for message flags, priority and date ranges are only available in mu +version 0.9 or later. + +Get all important messages which are signed: +.nf + \fB$ mu find flag:signed prio:high \fR +.fi + +Get all messages from Jim without an attachment: +.nf + \fB$ mu find from:jim AND NOT flag:attach\fR +.fi + +Get all messages where Jack is in one of the contact fields: +.nf + \fB$ mu find contact:jack\fR +.fi +This uses the special contact: pseudo-field which matches (\fBfrom\fR, +\fBto\fR, \fBcc\fR and \fBbcc\fR). + +Get all messages in the Sent Items folder about yoghurt: +.nf + \fB$mu find maildir:'/Sent Items' yoghurt\fR +.fi +Note how we need to quote search terms that include spaces. + + +Get all unread messages where the subject mentions Ångström: +.nf + \fB$ mu find subject:Ångström flag:unread\fR +.fi +which is equivalent to: +.nf + \fB$ mu find subject:angstrom flag:unread\fR +.fi +because does mu is case-insensitive and accent-insensitive. + +Get all unread messages between March 2002 and August 2003 about some bird (or +a Swedish rock band): +.nf + \fB$ mu find date:20020301..20030831 nightingale flag:unread\fR +.fi + +Get all messages received today: +.nf + \fB$ mu find date:today..now\fR +.fi + +Get all messages we got in the last two weeks about emacs: +.nf + \fB$ mu find date:2w..now emacs\fR +.fi + +Another powerful feature (since 0.9.6) are wildcard searches, where you can +search for the last \fIn\fR characters in a word. For example, you can search +for: +.nf + \fB$ mu find 'subject:soc*'\fR +.fi +and get mails about soccer, Socrates, society, and so on. Note, it's important +to quote the search query, otherwise the shell will interpret +the '*'. + +You can also search for messages with a certain attachment using their +filename, for example: + +.nf + \fB$ mu find 'file:pic*'\fR +.fi +will get you all messages with an attachment starting with 'pic'. + +If you want to find attachments with a certain MIME-type, you can use the +following: + +Get all messages with PDF attachments: +.nf + \fB$ mu find mime:application/pdf\fR +.fi + +or even: + +Get all messages with image attachments: +.nf + \fB$ mu find 'mime:image/*'\fR +.fi + + +Note that (1) the '*' wildcard can only be used as the rightmost thing in a +search query, and (2) that you need to quote the search term, because +otherwise your shell will interpret the '*' (expanding it to all files in the +current directory -- probably not what you want). + +.SH DISPLAYING MESSAGES + +We might also want to display the complete messages instead of the header +information. This can be done using \fBmu view\fR command. Note that this +command does not use the database; you simply provide it the path to a +message. + +Therefore, if you want to display some message from a search query, you'll +need its path. To get the path (think \fBl\fRocation) for our first example we +can use: + +.nf + \fB$ mu find --fields="l" t:julius fruit\fR +.fi + +And we'll get something like: +.nf + /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2, +.fi +We can now display this message: + +.nf + \fB$ mu view /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,\fR + + From: John Milton + To: Julius Caesar + Subject: Fere libenter homines id quod volunt credunt + Date: 2008-07-31T21:57:25 EEST + + OF Mans First Disobedience, and the Fruit + Of that Forbidden Tree, whose mortal taste + Brought Death into the World, and all our woe, + [...] +.fi + +.SH FINDING CONTACTS + +While \fBmu find\fR searches for messages, there is also \fBmu cfind\fR to +find \fIcontacts\fR, that is, names + addresses. Without any search +expression, \fBmu cfind\fR lists all of your contacts. + +.nf + \fB$ mu cfind julius\fR +.fi + +will find all contacts with 'julius' in either name or e-mail address. Note +that \fBmu cfind\fR accepts a \fIregular expression\fR. + +\fBmu cfind\fR also supports a \fI--format=\fR-parameter, which sets the +output to some specific format, so the results can be imported into another +program. For example, to export your contact information to a \fBmutt\fR +address book file, you can use something like: + +.nf + \fB$ mu cfind --format=mutt-alias > ~/mutt-aliases \fR +.fi + +Then, you can use them in \fBmutt\fR if you add something like \fBsource +~/mutt-aliases\fR to your \fImuttrc\fR. + +.SH AUTHOR +Dirk-Jan C. Binnema + +.SH "SEE ALSO" +.BR mu (1), +.BR mu-init (1), +.BR mu-index (1), +.BR mu-find (1), +.BR mu-mfind (1), +.BR mu-mkdir (1), +.BR mu-view (1), +.BR mu-extract (1) diff --git a/man/mu-extract.1 b/man/mu-extract.1 new file mode 100644 index 0000000..35d96ce --- /dev/null +++ b/man/mu-extract.1 @@ -0,0 +1,100 @@ +.TH MU EXTRACT 1 "July 2012" "User Manuals" + +.SH NAME + +\fBmu extract\fR is the \fBmu\fR command to display and save message parts +(attachments), and open them with other tools. + +.SH SYNOPSIS + +.B mu extract [options] + +.B mu extract [options] + +.SH DESCRIPTION + +\fBmu extract\fR is the \fBmu\fR sub-command for extracting MIME-parts (e.g., +attachments) from mail messages. The sub-command works on message files, and +does not require the message to be indexed in the database. + +For attachments, the file name used when saving it is the name of the +attachment in the message. If there is no such name, or when saving +non-attachment MIME-parts, a name is derived from the message-id of the +message. + +If you specify a pattern (a case-insensitive regular expression) as the second +argument, all attachments with filenames matching that pattern will be +extracted. The regular expressions are Perl-compatible (as per the +PCRE-library). + +Without any options, \fBmu extract\fR simply outputs the list of leaf +MIME-parts in the message. Only 'leaf' MIME-parts (including RFC822 +attachments) are considered, \fBmultipart/*\fR etc. are ignored. + +.SH OPTIONS + +.TP +\fB\-a\fR, \fB\-\-save\-attachments\fR +save all MIME-parts that look like attachments. + +.TP +\fB\-\-save\-all\fR +save all non-multipart MIME-parts. + +.TP +\fB\-\-parts\fR= +only consider the following numbered parts +(comma-separated list). The numbers for the parts can be seen from running +\fBmu extract\fR without any options but only the message file. + +.TP +\fB\-\-target\-dir\fR= +save the parts in the target directory rather than +the current working directory. + +.TP +\fB\-\-overwrite\fR +overwrite existing files with the same name; by default overwriting is not +allowed. + +.TP +\fB\-\-play\fR Try to 'play' (open) the attachment with the default +application for the particular file type. On MacOS, this uses the \fBopen\fR +program, on other platforms it uses \fBxdg-open\fR. You can choose a different +program by setting the \fBMU_PLAY_PROGRAM\fR environment variable. + +.SH EXAMPLES + +To display information about all the MIME-parts in a message file: +.nf + $ mu extract msgfile +.fi + +To extract MIME-part 3 and 4 from this message, overwriting existing files +with the same name: +.nf + $ mu extract --parts=3,4 --overwrite msgfile +.fi + +To extract all files ending in '.jpg' (case-insensitive): +.nf + $ mu extract msgfile '.*\.jpg' +.fi + +To extract an mp3-file, and play it in the default mp3-playing application: +.nf + $ mu extract --play msgfile 'whoopsididitagain.mp3' +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1) diff --git a/man/mu-fields.1 b/man/mu-fields.1 new file mode 100644 index 0000000..e86c22a --- /dev/null +++ b/man/mu-fields.1 @@ -0,0 +1,32 @@ +.TH MU FIELDS 1 "April 2022" "User Manuals" + +.SH NAME + +mu fields\- list all message fields + +.SH SYNOPSIS + +.B mu fields [options] + +.SH DESCRIPTION + +\fBmu fields\fR is the \fBmu\fR command for showing a table of message fields +and their properties. + +.SH OPTIONS + +Inherits common options from +.BR mu(1) + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1) diff --git a/man/mu-find.1 b/man/mu-find.1 new file mode 100644 index 0000000..df2f4d8 --- /dev/null +++ b/man/mu-find.1 @@ -0,0 +1,338 @@ +.TH MU FIND 1 "29 April 2022" "User Manuals" + +.SH NAME + +mu find \- find e-mail messages in the \fBmu\fR database. + +.SH SYNOPSIS + +.B mu find [options] + +.SH DESCRIPTION + +\fBmu find\fR is the \fBmu\fR command for searching e-mail message +that were stored earlier using \fBmu index\fR(1). + +.SH SEARCHING MAIL + +\fBmu find\fR starts a search for messages in the database that match +some search pattern. The search patterns are described in detail in +.BR mu-query (7). +. + +For example: + +.nf + $ mu find subject:snow and date:2009.. +.fi + +would find all messages in 2009 with 'snow' in the subject field, e.g: + +.nf + 2009-03-05 17:57:33 EET Lucia running in the snow + 2009-03-05 18:38:24 EET Marius Re: running in the snow +.fi + +Note, this the default, plain-text output, which is the default, so you don't +have to use \fB--format=plain\fR. For other types of output (such as symlinks, +XML or s-expressions), see the discussion in the \fBOPTIONS\fR-section below +about \fB--format\fR. + +The search pattern is taken as a command-line parameter. If the search +parameter consists of multiple parts (as in the example) they are +treated as if there were a logical \fBand\fR between them. + +For details on the possible queries, see +.BR mu-query (7). + +.SH OPTIONS + +Note, some of the important options are described in the \fBmu\fR(1) man-page +and not here, as they apply to multiple mu-commands. + +The \fBfind\fR-command has various options that influence the way \fBmu\fR +displays the results. If you don't specify anything, the defaults are +\fI\-\-fields="d f s"\fR, \fI\-\-sortfield=date\fR and \fI\-\-reverse\fR. + +.TP +\fB\-f\fR, \fB\-\-fields\fR=\fI\fR +specifies a string that determines which fields are shown in the output. This +string consists of a number of characters (such as 's' for subject or 'f' for +from), which will replace with the actual field in the output. Fields that are +not known will be output as-is, allowing for some simple formatting. + +For example: + +.nf + $ mu find subject:snow --fields "d f s" +.fi + +would list the date, subject and sender of all messages with 'snow' in the +their subject. + +The table of replacement characters is superset of the list mentions for +search parameters, such as: +.nf + t \fBt\fRo: recipient + d Sent \fBd\fRate of the message + f Message sender (\fBf\fRrom:) + g Message flags (fla\fBg\fRs) + l Full path to the message (\fBl\fRocation) + s Message \fBs\fRubject + i Message-\fBi\fRd + m \fBm\fRaildir +.fi + +For the complete, up-to-date list, see: +.BR mu-fields(1) + +The message flags are described in \fBmu-query\fR(7). As an example, a +message which is 'seen', has an attachment and is signed would +have 'asz' as its corresponding output string, while an encrypted new +message would have 'nx'. + +.TP +\fB\-s\fR, \fB\-\-sortfield\fR \fR=\fI\fR and \fB\-z\fR, +\fB\-\-reverse\fR specifies the field to sort the search results by, and the +direction (i.e., 'reverse' means that the sort should be reverted - Z-A). Examples include: + +.nf + cc,c Cc (carbon-copy) recipient(s) + date,d Message sent date + from,f Message sender + maildir,m Maildir + msgid,i Message id + prio,p Nessage priority + subject,s Message subject + to,t To:-recipient(s) +.fi + +For the complete list use can use the \fBmu fields\fR command; see: +.BR mu-fields(1) + +Thus, for example, to sort messages by date, you could specify: + +.nf + $ mu find fahrrad --fields "d f s" --sortfield=date --reverse +.fi + +Note, if you specify a sortfield, by default, messages are sorted in reverse +(descending) order (e.g., from lowest to highest). This is usually a good +choice, but for dates it may be more useful to sort in the opposite direction. + +.TP +\fB\-n\fR, \fB\-\-maxnum=\fR +If > 0, display maximally that number of entries. If not specified, all matching entries are displayed. + +.TP +\fB\-\-summary-len=\fR +If > 0, use that number of lines of the message to provide a summary. + +.TP +\fB\-\-format\fR=\fIplain|links|xquery|xml|sexp\fR +output results in the specified format. + +The default is \fBplain\fR, i.e normal output with one line per message. + +\fBlinks\fR outputs the results as a maildir with symbolic links to the found +messages. This enables easy integration with mail-clients (see below for more +information). + +\fBxml\fR formats the search results as XML. + +\fBsexp\fR formats the search results as an s-expression as used in Lisp +programming environments. + +\fBxquery\fR shows the Xapian query corresponding to your search terms. This +is meant for for debugging purposes. + +.TP +\fB\-\-linksdir\fR \fR=\fI\fR and \fB\-c\fR, \fB\-\-clearlinks\fR +output the results as a maildir with symbolic links to the found +messages. This enables easy integration with mail-clients (see below +for more information). \fBmu\fR will create the maildir if it does not +exist yet. + +If you specify \fB\-\-clearlinks\fR, all existing symlinks will be +cleared from the target directories; this allows for re-use of the +same maildir. However, this option will delete any symlink it finds, +so be careful. + +.nf + $ mu find grolsch --format=links --linksdir=~/Maildir/search --clearlinks +.fi + +will store links to found messages in \fI~/Maildir/search\fR. If the directory +does not exist yet, it will be created. + +Note: when \fBmu\fR creates a Maildir for these links, it automatically +inserts a \fI.noindex\fR file, to exclude the directory from \fBmu +index\fR. + +.TP +\fB\-\-after=\fR\fI\fR only show messages whose message files were +last modified (\fBmtime\fR) after \fI\fR. \fI\fR is a +UNIX \fBtime_t\fR value, the number of seconds since 1970-01-01 (in UTC). + +From the command line, you can use the \fBdate\fR command to get this +value. For example, only consider messages modified (or created) in the last 5 +minutes, you could specify +.nf + --after=`date +%s --date='5 min ago'` +.fi +This is assuming the GNU \fBdate\fR command. + + +.TP +\fB\-\-exec\fR=\fI\fR +the \fB\-\-exec\fR command causes the \fIcommand\fR to be executed on each +matched message; for example, to see the raw text of all messages +matching 'milkshake', you could use: +.nf + $ mu find milkshake --exec='less' +.fi +which is roughly equivalent to: +.nf + $ mu find milkshake --fields="l" | xargs less +.fi + + +.TP +\fB\-b\fR, \fB\-\-bookmark\fR=\fI\fR +use a bookmarked search query. Using this option, a query from your bookmark +file will be prepended to other search queries. See \fBmu-bookmarks\fR(1) for the +details of the bookmarks file. + + +.TP +\fB\-\-skip\-dups\fR,\fB-u\fR whenever there are multiple messages with the +same name, only show the first one. This is useful if you have copies of the +same message, which is a common occurrence when using e.g. Gmail together with +\fBofflineimap\fR. + +.TP +\fB\-\-include\-related\fR,\fB-r\fR also include messages being referred to by +the matched messages -- i.e.. include messages that are part of the same +message thread as some matched messages. This is useful if you want +Gmail-style 'conversations'. Note, finding these related messages make +searches slower. + +.TP +\fB\-t\fR, \fB\-\-threads\fR show messages in a 'threaded' format -- +that is, with indentation and arrows showing the conversation threads +in the list of matching messages. When using this, sorting is +chronological (by date), based on the newest message in a thread. + +Messages in the threaded list are indented based on the depth in the +discussion, and are prefix with a kind of arrow with thread-related +information about the message, as in the following table: + +.nf +| | normal | orphan | duplicate | +|-------------+--------+--------+-----------| +| first child | `-> | `*> | `=> | +| other | |-> | |*> | |=> | +.fi + +Here, an 'orphan' is a message without a parent message (in the list of +matches), and a duplicate is a message whose message-id was already seen +before; not this may not really be the same message, if the message-id was +copied. + +The algorithm used for determining the threads is based on Jamie Zawinksi's +description: +.BR http://www.jwz.org/doc/threading.html + + +.SS Integrating mu find with mail clients + +.TP + +\fBmutt\fR + +For \fBmutt\fR you can use the following in your \fImuttrc\fR; pressing the F8 +key will start a search, and F9 will take you to the results. + +.nf +# mutt macros for mu +macro index "mu find --clearlinks --format=links --linksdir=~/Maildir/search " \\ + "mu find" +macro index "~/Maildir/search" \\ + "mu find results" +.fi + + +.TP + +\fBWanderlust\fR + +\fBSam B\fR suggested the following on the \fBmu\fR-mailing list. First add +the following to your Wanderlust configuration file: + +.nf +(require 'elmo-search) +(elmo-search-register-engine + 'mu 'local-file + :prog "/usr/local/bin/mu" ;; or wherever you've installed it + :args '("find" pattern "--fields" "l") :charset 'utf-8) + +(setq elmo-search-default-engine 'mu) +;; for when you type "g" in folder or summary. +(setq wl-default-spec "[") +.fi + +Now, you can search using the \fBg\fR key binding; you can also create +permanent virtual folders when the messages matching some expression by adding +something like the following to your \fIfolders\fR file. + +.nf +VFolders { + [date:today..now]!mu "Today" + + [size:1m..100m]!mu "Big" + + [flag:unread]!mu "Unread" +} +.fi + +After restarting Wanderlust, the virtual folders should appear. + +.SH RETURN VALUE + +\fBmu find\fR returns 0 upon successful completion; if the search was +performed, there needs to be a least one match. Anything else leads to a +non-zero return value, for example: + +.nf +| code | meaning | +|------+--------------------------------| +| 0 | ok | +| 1 | general error | +| 4 | no matches (for 'mu find') | +.fi + + +.SH ENCODING + +\fBmu find\fR output is encoded according the locale for \fI--format=plain\fR +(the default), and UTF-8 for all other formats (\fIsexp\fR, +\fIxml\fR). + + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues +If you have specific messages which are not matched correctly, please attach +them (appropriately censored if needed). + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-query (7) +.BR mu-fields (1) diff --git a/man/mu-help.1 b/man/mu-help.1 new file mode 100644 index 0000000..80ba090 --- /dev/null +++ b/man/mu-help.1 @@ -0,0 +1,34 @@ +.TH MU HELP 1 "July 2012" "User Manuals" + +.SH NAME + +\fBmu help\fR is a \fBmu\fR command that gives help information about mu +commands. + +.SH SYNOPSIS + +.B mu help + +.SH DESCRIPTION + +\fBmu help\fR provides help information about mu commands. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu-index (1), +.BR mu-find (1), +.BR mu-cfind (1), +.BR mu-mkdir (1), +.BR mu-view (1), +.BR mu-extract (1), +.BR mu-easy (1), +.BR mu-bookmarks (5) diff --git a/man/mu-index.1 b/man/mu-index.1 new file mode 100644 index 0000000..d9e9ea3 --- /dev/null +++ b/man/mu-index.1 @@ -0,0 +1,196 @@ +.TH MU-INDEX 1 "June 2022" "User Manuals" + +.SH NAME + +mu index \- index e-mail messages stored in Maildirs + +.SH SYNOPSIS + +.B mu index [options] + +.SH DESCRIPTION + +\fBmu index\fR is the \fBmu\fR command for scanning the contents of Maildir +directories and storing the results in a Xapian database. The data can then be +queried using +.BR mu-find (1)\. + +Before the first time you run \fBmu index\fR, you must run \fBmu init\fR to +initialize the database. + +\fBindex\fR understands Maildirs as defined by Daniel Bernstein for +\fBqmail\fR(7). In addition, it understands recursive Maildirs (Maildirs within +Maildirs), Maildir++. It can also deal with VFAT-based Maildirs which use '!' +or ';' as the separators instead of ':'. + +E-mail messages which are not stored in something resembling a maildir +leaf-directory (\fIcur\fR and \fInew\fR) are ignored, as are the cache +directories for \fInotmuch\fR and \fIgnus\fR, and any dot-directory. + +Starting with mu 1.5.x, symlinks are followed, and can be spread over multiple +filesystems; however note that moving files around is much faster when multiple +filesystems are not involved. + +If there is a file called \fI.noindex\fR in a directory, the contents of that +directory and all of its subdirectories will be ignored. This can be useful to +exclude certain directories from the indexing process, for example directories +with spam-messages. + +If there is a file called \fI.noupdate\fR in a directory, the contents of that +directory and all of its subdirectories will be ignored, unless we do a full +rebuild (with \fBmu init\fR). This can be useful to speed up things you have +some maildirs that never change. Note that you can still search for these +messages, this only affects updating the database. \fI.noupdate\fR is ignored +when you start indexing with an empty database (such as directly after \fImu +init\fR. + +There also the \fB--lazy-check\fR which can greatly speed up indexing; see below +for details. + +The first run of \fBmu index\fR may take a few minutes if you have a lot of mail +(tens of thousands of messages). Fortunately, such a full scan needs to be done +only once; after that it suffices to index the changes, which goes much faster. +See the 'Note on performance (i,ii,iii)' below for more information. + +The optional 'phase two' of the indexing-process is the removal of messages from +the database for which there is no longer a corresponding file in the Maildir. +If you do not want this, you can use \fB\-n\fR, \fB\-\-nocleanup\fR. + +When \fBmu index\fR catches one of the signals \fBSIGINT\fR, \fBSIGHUP\fR or +\fBSIGTERM\fR (e.g., when you press Ctrl-C during the indexing process), it +tries to shutdown gracefully; it tries to save and commit data, and close the +database etc. If it receives another signal (e.g., when pressing Ctrl-C once +more), \fBmu index\fR will terminate immediately. + +.SH OPTIONS + +Some of the general options are described in the \fBmu(1)\fR man-page and not +here, as they apply to multiple mu commands. + +.TP +\fB\-\-lazy-check\fR +in lazy-check mode, \fBmu\fR does not consider messages for which the +time-stamp (ctime) of the directory they reside in has not changed +since the previous indexing run. This is much faster than the non-lazy +check, but won't update messages that have change (rather than having +been added or removed), since merely editing a message does not update +the directory time-stamp. Of course, you can run \fBmu-index\fR +occasionally without \fB\-\-lazy-check\fR, to pick up such messages. + +.TP +\fB\-\-nocleanup\fR +disables the database cleanup that \fBmu\fR does by default after indexing. + +.SS A note on performance (i) +As a non-scientific benchmark, a simple test on the author's machine (a +Thinkpad X61s laptop using Linux 2.6.35 and an ext3 file system) with no +existing database, and a maildir with 27273 messages: + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 66,65s user 6,05s system 27% cpu 4:24,20 total +.fi +(about 103 messages per second) + +A second run, which is the more typical use case when there is a database +already, goes much faster: + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 0,48s user 0,76s system 10% cpu 11,796 total +.fi +(more than 56818 messages per second) + +Note that each test flushes the caches first; a more common use case might +be to run \fBmu index\fR when new mail has arrived; the cache may stay +quite 'warm' in that case: + +.nf + $ time mu index --quiet + 0,33s user 0,40s system 80% cpu 0,905 total +.fi +which is more than 30000 messages per second. + + +.SS A note on performance (ii) +As per June 2012, we did the same non-scientific benchmark, this time with an +Intel i5-2500 CPU @ 3.30GHz, an ext4 file system and a maildir with 22589 +messages. We start without an existing database. + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 27,79s user 2,17s system 48% cpu 1:01,47 total +.fi +(about 813 messages per second) + +A second run, which is the more typical use case when there is a database +already, goes much faster: + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 0,13s user 0,30s system 19% cpu 2,162 total +.fi +(more than 173000 messages per second) + + +.SS A note on performance (iii) +As per July 2016, we did the same non-scientific benchmark, again with +the Intel i5-2500 CPU @ 3.30GHz, an ext4 file system. This time, the +maildir contains 72525 messages. + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 40,34s user 2,56s system 64% cpu 1:06,17 total +.fi +(about 1099 messages per second). + +.SS A note on performance (iv) +A few years later and its June 2022. There's a lot more happening during indexing, but indexing became multi-threaded and machines are faster; e.g. this +is with an AMD Ryzen Threadripper 1950X (32) @ 3.399GHz. + +The instructions are a little different since we have a proper repeatable +benchmark now. After building, + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' +% THREAD_NUM=4 build/lib/tests/bench-indexer -m perf +# random seed: R02Sf5c50e4851ec51adaf301e0e054bd52b +1..1 +# Start of bench tests +# Start of indexer tests +indexed 5000 messages in 20 maildirs in 3763ms; 752 μs/message; 1328 messages/s (4 thread(s)) +ok 1 /bench/indexer/4-cores +# End of indexer tests +# End of bench tests +.fi + +Things are again a little faster, even though the index does a lot more now +(text-normalizatian, and pre-generating message-sexps). A faster machine helps, +too! + +.SH RETURN VALUE + +\fBmu index\fR return 0 upon successful completion; any other number signals an +error. + +.SH BUGS + +Please report bugs if you find any: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR mu-init (1), +.BR mu-find (1), +.BR mu-cfind (1) diff --git a/man/mu-info.1 b/man/mu-info.1 new file mode 100644 index 0000000..ea8e703 --- /dev/null +++ b/man/mu-info.1 @@ -0,0 +1,47 @@ +.TH MU-INFO 1 "May 2022" "User Manuals" + +.SH NAME + +mu info \- show information about the mu database + +.SH SYNOPSIS + +.B mu info [options] + +.SH DESCRIPTION + +\fBmu info\fR is the \fBmu\fR command for getting information about the mu +database. Note that while running (e.g. \fBmu4e\fR), some of the information +may be slightly delayed due to database caching. + +.SH OPTIONS + +Note, some of the general options are described in the \fBmu(1)\fR man-page and +not here, as they apply to multiple mu commands. + +.TP +\fB\-\-muhome\fR +use an alternative directory to store and read the database, write the logs, +etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux +this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of +\fBmu\fR defaulted to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR. + +.SH RETURN VALUE + +\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if +there was some error. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR mu-index (1) diff --git a/man/mu-init.1 b/man/mu-init.1 new file mode 100644 index 0000000..e5697d4 --- /dev/null +++ b/man/mu-init.1 @@ -0,0 +1,79 @@ +.TH MU-INIT 1 "May 2022" "User Manuals" + +.SH NAME + +mu init \- initialize the mu message database + +.SH SYNOPSIS + +.B mu init [options] + +.SH DESCRIPTION + +\fBmu init\fR is the subcommand for setting up the mu message +database. After \fBmu init\fR has completed, you can run \fBmu +index\fR + +.SH OPTIONS + +Note, some of the general options are described in the \fBmu(1)\fR +man-page and not here, as they apply to multiple mu commands. + +.TP +\fB\-\-muhome\fR +use an alternative directory to store and read the database, write the logs, +etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux +this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR). + +Earlier versions of \fBmu\fR defaulted to \fI~/.mu\fR, which now requires +\fI\-\-muhome=~/.mu\fR. + +Alternatively, use can use the \fBMUHOME\fR environment variable (the command-line takes precedence). + +.TP +\fB\-m\fR, \fB\-\-maildir\fR=\fI\fR +starts searching at \fI\fR. By default, \fBmu\fR uses whatever the +\fBMAILDIR\fR environment variable is set to; if it is not set, it tries +\fI~/Maildir\fR if it already exists. + +.TP +\fB\-\-my-address\fR=\fI\fR +specifies that some e-mail addresses are 'my-address' (\fB\-\-my-address\fR can +be used multiple times). This is used by \fBmu cfind\fR -- any e-mail address +found in the address fields of a message which also has \fI\fR +in one of its address fields is considered a \fIpersonal\fR e-mail address. This +allows you, for example, to filter out (\fBmu cfind --personal\fR) addresses +which were merely seen in mailing list messages. + +\fI\fR can be either a plain e-mail address (such as +\fBfoo@example.com\fR), or a regular-expression (of the 'Basic POSIX' flavor), +wrapped in \fB/\fR (such as \fB/foo-.*@example\\.com/\fR). Depending on your +shell program, the argument may need to b quoted. + +.SH ENVIRONMENT + +\fBmu init\fR uses \fBMAILDIR\fR to find the user's Maildir if it has not been +specified explicitly with \fB\-\-maildir\fR=\fI\fR. If \fBMAILDIR\fR is +not set, \fBmu init\fR uses \fI~/Maildir\fR. + +\fBMUHOME\fR can be used as an alternative to \fB\-\-muhome\fR. + +.SH RETURN VALUE + +\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if +there was some error. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR mu-index (1) diff --git a/man/mu-mkdir.1 b/man/mu-mkdir.1 new file mode 100644 index 0000000..6f9ddd2 --- /dev/null +++ b/man/mu-mkdir.1 @@ -0,0 +1,45 @@ +.TH MU MKDIR 1 "July 2012" "User Manuals" + +.SH NAME + +mu mkdir\- create a new Maildir + +.SH SYNOPSIS + +.B mu mkdir [options] [] + +.SH DESCRIPTION + +\fBmu mkdir\fR is the \fBmu\fR command for creating Maildirs. It does +\fBnot\fR use the mu database. With the \fBmkdir\fR command, you can create +new Maildirs with permissions 0755. For example, + +.nf + mu mkdir tom dick harry +.fi + +creates three maildirs, \fItom\fR, \fIdick\fR and \fIharry\fR. + +If creation fails for any reason, \fBno\fR attempt is made to remove any parts +that were created. This is for safety reasons. + +.SH OPTIONS + +.TP +\fB\-\-mode\fR= +set the file access mode for the new maildir(s) as in \fBchmod(1)\fR. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR chmod (1) diff --git a/man/mu-query.7 b/man/mu-query.7 new file mode 100644 index 0000000..ca1800a --- /dev/null +++ b/man/mu-query.7 @@ -0,0 +1,361 @@ +.TH MU QUERY 7 "22 April 2022" "User Manuals" + +.SH NAME + +mu query language \- a language for finding messages in \fBmu\fR databases. + +.SH DESCRIPTION + +The mu query language is a language used by \fBmu find\fR and \fBmu4e\fR to find +messages in \fBmu\fR's Xapian databases. The language is quite similar to +Xapian's default query-parser, but is an independent implementation that is +customized for the mu/mu4e use-case. + +In this article, we give a structured but informal overview of the query +language and provide examples. + +As a companion to this, we recommend the \fBmu fields\fR and \fBmu flags\fR +commands to get an up-to-date list of the available fields and flags. + +\fBNOTE:\fR if you use queries on the command-line (say, for \fBmu find\fR), you +need to quote any characters that would otherwise be interpreted by the shell, +such as \fB""\fR, \fB(\fR and \fB)\fR and whitespace. + +.de EX1 +.nf +.RS +.. + +.de EX2 +.RE +.fi +.. + +.SH TERMS + +The basic building blocks of a query are \fBterms\fR; these are just +normal words like 'banana' or 'hello', or words prefixed with a +field-name which make them apply to just that field. See +\fBmu find\fR +for all the available fields. + +Some example queries: +.EX1 +vacation +subject:capybara +maildir:/inbox +.EX2 + +Terms without an explicit field-prefix, (like 'vacation' above) are +interpreted like: +.EX1 +to:vacation or subject:vacation or body:vacation or ... +.EX2 + +The language is case-insensitive for terms and attempts to 'flatten' +any diacritics, so \fIangtrom\fR matches \fIÅngström\fR. + +.PP +If terms contain whitespace, they need to be quoted: +.EX1 +subject:"hi there" +.EX2 +This is a so-called \fIphrase query\fR, which means that we match +against subjects that contain the literal phrase "hi there". + +Remember that you need to escape those quotes when using this from the +command-line: +.EX1 +mu find subject:\\"hi there\\" +.EX2 + +.SH LOGICAL OPERATORS + +We can combine terms with logical operators -- binary ones: \fBand\fR, +\fBor\fR, \fBxor\fR and the unary \fBnot\fR, with the conventional +rules for precedence and association, and are case-insensitive. + +.PP +You can also group things with \fB(\fR and \fB)\fR, so you can do +things like: +.EX1 +(subject:beethoven or subject:bach) and not body:elvis +.EX2 + +If you do not explicitly specify an operator between terms, \fBand\fR +is implied, so the queries +.EX1 +subject:chip subject:dale +.EX2 +.EX1 +subject:chip AND subject:dale +.EX2 +are equivalent. For readability, we recommend the second version. + +Note that a \fIpure not\fR - e.g. searching for \fBnot apples\fR is +quite a 'heavy' query. + +.SH REGULAR EXPRESSIONS AND WILDCARDS + +The language supports matching regular expressions that follow +ECMAScript; for details, see + +.BR http://www.cplusplus.com/reference/regex/ECMAScript/ + +Regular expressions must be enclosed in \fB//\fR. Some examples: +.EX1 +subject:/h.llo/ # match hallo, hello, ... +subject:/ +.EX2 + +Note the difference between 'maildir:/foo' and 'maildir:/foo/'; the +former matches messages in the '/foo' maildir, while the latter +matches all messages in all maildirs that match 'foo', such +as '/foo', '/bar/cuux/foo', '/fooishbar' etc. + +Wildcards are an older mechanism for matching where a term with a +rightmost \fB*\fR (and \fIonly\fR in that position) matches any term +that starts with the part before the \fB*\fR; they are supported for +backward compatibility and \fBmu\fR translates them to regular +expressions internally: +.EX1 +foo* +.EX2 +is equivalent to +.EX1 +/foo.*/ +.EX2 + +As a note of caution, certain wild-cards and regular expression can +take quite a bit longer than 'normal' queries. + +.SH FIELDS + +We already saw a number of search fields, such as \fBsubject:\fR and +\fBbody:\fR. For the full table, see \fBmu fields\fR. +.EX1 + bcc,h Bcc (blind-carbon-copy) recipient(s) + body,b Message body + cc,c Cc (carbon-copy) recipient(s) + changed,k Last change to message file (range) + date,d Send date (range) + embed,e Search inside embedded text parts + file,j Attachment filename + flag,g Message Flags + from,f Message sender + list,v Mailing list (e.g. the List-Id value) + maildir,m Maildir + mime,y MIME-type of one or more message parts + msgid,i Message-ID + prio,p Message priority (\fIlow\fR, \fInormal\fR or \fIhigh\fR) + size,z Message size range + subject,s Message subject + tag,x Tags for the message + thread,w Thread a message belongs to + to,t To: recipient(s) + +The \fBmu fields\fR command is recommended to get the latest version. +.EX2 +The shortcut character can be used instead of the full name: +.EX1 +f:foo@bar +.EX2 +is the same as +.EX1 +from:foo@bar +.EX2 +For queries that are not one-off, we would recommend the longer name +for readability. + +There are also the special fields \fBcontact:\fR, which matches all +contact-fields (\fIfrom\fR, \fIto\fR, \fIcc\fR and \fIbcc\fR), and +\fBrecip\fR, which matches all recipient-fields (\fIto\fR, \fIcc\fR +and \fIbcc\fR). Hence, for instance, +.EX1 +contact:fnorb@example.com +.EX2 +is equivalent to +.EX1 +(from:fnorb@example.com or to:fnorb@example.com or + cc:from:fnorb@example.com or bcc:fnorb@example.com) +.EX2 + +.SH DATE RANGES + +The \fBdate:\fR field takes a date-range, expressed as the lower and +upper bound, separated by \fB..\fR. Either lower or upper (but not +both) can be omitted to create an open range. + +Dates are expressed in local time and using ISO-8601 format +(YYYY-MM-DD HH:MM:SS); you can leave out the right part, and \fBmu\fR +adds the rest, depending on whether this is the beginning or end of +the range (e.g., as a lower bound, '2015' would be interpreted as the +start of that year; as an upper bound as the end of the year). + +You can use '/' , '.', '-' and 'T' to make dates more human readable. + +Some examples: +.EX1 +date:20170505..20170602 +date:2017-05-05..2017-06-02 +date:..2017-10-01T12:00 +date:2015-06-01.. +date:2016..2016 +.EX2 + +You can also use the special 'dates' \fBnow\fR and \fBtoday\fR: +.EX1 +date:20170505..now +date:today.. +.EX2 + +Finally, you can use relative 'ago' times which express some time +before now and consist of a number followed by a unit, with units +\fBs\fR for seconds, \fBM\fR for minutes, \fBh\fR for hours, \fBd\fR +for days, \fBw\fR for week, \fBm\fR for months and \fBy\fR for years. +Some examples: + +.EX1 +date:3m.. +date:2017.01.01..5w +.EX2 + +.SH SIZE RANGES + +The \fBsize\fR or \fBz\fR field allows you to match \fIsize ranges\fR +-- that is, match messages that have a byte-size within a certain +range. Units (b (for bytes), K (for 1000 bytes) and M (for 1000 * 1000 +bytes) are supported). Some examples: + +.EX1 +size:10k..2m +size:10m.. +.EX2 + +.SH FLAG FIELDS + +The \fBflag\fR/\fBg\fR field allows you to match message flags. The +following fields are available: +.EX1 + a,attach Message with attachment + d,draft Draft Message + f,flagged Flagged + l,list Mailing-list message + n,new New message (in new/ Maildir) + p,passed Passed ('Handled') + r,replied Replied + s,seen Seen + t,trashed Marked for deletion + u,unread new OR NOT seen + x,encrypted Encrypted message + z,signed Signed message +.EX2 + +Some examples: +.EX1 +flag:attach +flag:replied +g:x +.EX2 + +Encrypted messages may be signed as well, but this is only visible +after decrypting and thus, invisible to \fBmu\fR. + +.SH PRIORITY FIELD + +The message priority field (\fBprio:\fR) has three possible values: +\fBlow\fR, \fBnormal\fR or \fBhigh\fR. For instance, to match +high-priority messages: +.EX1 + prio:high +.EX2 + +.SH MAILDIR + +The Maildir field describes the directory path starting \fBafter\fR +the Maildir-base path, and before the \fI/cur/\fR or \fI/new/\fR part. +So for example, if there's a message with the file name +\fI~/Maildir/lists/running/cur/1234.213:2,\fR, you could find it (and +all the other messages in the same maildir) with: +.EX1 +maildir:/lists/running +.EX2 + +Note the starting '/'. If you want to match mails in the 'root' +maildir, you can do with a single '/': +.EX1 +maildir:/ +.EX2 + +If you have maildirs (or any fields) that include spaces, you need to +quote them, ie. +.EX1 +maildir:"/Sent Items" +.EX2 + +Note that from the command-line, such queries must be quoted: +.EX1 +mu find 'maildir:"/Sent Items"' +.EX2 + +.SH MORE EXAMPLES + +Here are some simple examples of \fBmu\fR queries; you can make many +more complicated queries using various logical operators, parentheses +and so on, but in the author's experience, it's usually faster to find +a message with a simple query just searching for some words. + +Find all messages with both 'bee' and 'bird' (in any field) +.EX1 +bee AND bird +.EX2 + +Find all messages with either Frodo or Sam: +.EX1 +Frodo OR Sam +.EX2 + +Find all messages with the 'wombat' as subject, and 'capybara' anywhere: +.EX1 +subject:wombat and capybara +.EX2 + +Find all messages in the 'Archive' folder from Fred: +.EX1 +from:fred and maildir:/Archive +.EX2 + +Find all unread messages with attachments: +.EX1 +flag:attach and flag:unread +.EX2 + + +Find all messages with PDF-attachments: +.EX1 +mime:application/pdf +.EX2 + +Find all messages with attached images: +.EX1 +mime:image/* +.EX2 + +.SH CAVEATS + +With current Xapian versions, the apostroph character is considered part of a +word. Thus, you cannot find \fID'Artagnan\fR by searching for \fIArtagnan\fR. +So, include the apostroph in search or use a regexp search. + +Matching on spaces has changed compared to the old query-parser; this applies +e.g. to Maildirs that have spaces in their name, such as \fISent Items\fR. See +\fBMAILDIR\fR above. + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu-find (1) +.BR mu-fields (1) diff --git a/man/mu-remove.1 b/man/mu-remove.1 new file mode 100644 index 0000000..e0aa212 --- /dev/null +++ b/man/mu-remove.1 @@ -0,0 +1,48 @@ +.TH MU REMOVE 1 "May 2022" "User Manuals" + +.SH NAME + +\fBmu remove\fR is the \fBmu\fR command to remove messages from the database. + +.SH SYNOPSIS + +.B mu remove [options] [] + +.SH DESCRIPTION + +\fBmu remove\fR removes specific messages from the database, each of them +specified by their filename. The files do not have to exist in the file +system. + +.SH OPTIONS + +\fBmu remove\fR does not have its own options, but the general options for +determining the location of the database (\fI--muhome\fR) are available. See +\fBmu-index(1)\fR for more information. + +.SH RETURN VALUE + +\fBmu remove\fR returns 0 upon success; in general, the following error codes are +returned: + +.nf +| code | meaning | +|------+-----------------------------------| +| 0 | ok | +| 1 | general error | +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-add (1) diff --git a/man/mu-script.1 b/man/mu-script.1 new file mode 100644 index 0000000..ec3f032 --- /dev/null +++ b/man/mu-script.1 @@ -0,0 +1,84 @@ +.TH MU SCRIPT 1 "October 2021" "User Manuals" + +.SH NAME + +mu script\- show the available mu scripts, and/or run them. + +.SH SYNOPSIS + +.B mu script [options] [] + +.B mu [] + +.SH DESCRIPTION + +\fBmu script\fR is the \fBmu\fR command to list available \fBmu\fR scripts. +The scripts are to be implemented in the Guile programming language, and +therefore only work if your \fBmu\fR is built with support for Guile. In +addition, many scripts require you to have \fBgnuplot\fR installed. + +Without any parameters, \fBmu script\fR lists the available scripts. If you +provide a pattern (a regular expression), only the scripts whose name or +one-line description match this pattern are listed. See the examples below. + +\fBmu\fR ships with a number of scripts. + +.SH OPTIONS + +.TP +\fB\-\-verbose\fR,\fB\-v\fR +when listing the available scripts, show the long descriptions. + +\fB\-\-\fR +all options on the right side of the \fB\-\-\fR are passed to the script. + +.SH EXAMPLES + +List all available scripts (one-line descriptions): +.nf + $ mu script +.fi + +List all available scripts matching \fImonth\fR (long descriptions): +.nf + $ mu script -v month +.fi + +Run the \fImsgs-per-month\fR script for messages matching 'hello', and pass it +the \fI--textonly\fR parameter: +.nf + $ mu msgs-per-month --query=hello --textonly +.fi + +.SH RETURN VALUE + +\fBmu script\fR returns 0 when all went well, and returns some non-zero error +code when this is not the case. + +.SH FILES + +You can make your own Scheme scripts accessible through \fBmu script\fR by +putting them in either \fI/mu/scripts\fR (e.g., \fI~/.local/share/mu/scripts\fR) or, if \fImuhome\fR is specified, in + +It is a good idea to document the scripts by using some +special comments in the source code: +.nf +;; INFO: this is my script -- one-line description +;; INFO: (longer description) +;; INFO: --option1= (describe option1) +;; INFO: etc. +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR guile (1) diff --git a/man/mu-server.1 b/man/mu-server.1 new file mode 100644 index 0000000..087483c --- /dev/null +++ b/man/mu-server.1 @@ -0,0 +1,61 @@ +.TH MU-SERVER 1 "January 2020" "User Manuals" + +.SH NAME + +mu server \- the mu backend for the mu4e e-mail client + +.SH SYNOPSIS + +.B mu server [options] + +.SH DESCRIPTION + +\fBmu server\fR starts a simple shell in which one can query and manipulate the +mu database. The output uses s-expressions. \fBmu server\fR is not meant for use +by humans, except for debugging purposes. Instead, it is designed specifically +for the \fBmu4e\fR e-mail client. + +In this man-page, we document the commands \fBmu server\fR accepts, as well as +their responses. In general, the commands sent to the server are s-expressions +of the form: + +.nf + ( :param1 value1 :param2 value2) +.fi + +For example, to view a certain message, the command would be: + +.nf + (view :docid 12345) +.fi + +Parameters can be sent in any order; they must be of the correct type though. +See \fBlib/utils/mu-sexp-parser.hh\fR and \fBlib/utils/mu-sexp-parser.cc\fR in +source-tree for the details. + + +.SH OUTPUT FORMAT + +\fBmu server\fR accepts a number of commands, and delivers its results in +the form: + +.nf + \\376\\377 +.fi + +\\376 (one byte 0xfe), followed by the length of the s-expression expressed as +an hexadecimal number, followed by another \\377 (one byte 0xff), followed by +the actual s-expression. + +By prefixing the expression with its length, it can be processed more +efficiently. The \\376 and \\377 were chosen since they never occur in valid +UTF-8 (in which the s-expressions are encoded). + +.sh COMMANDS + + +.SH AUTHOR +Dirk-Jan C. Binnema + +.SH "SEE ALSO" +.BR mu (1) diff --git a/man/mu-verify.1 b/man/mu-verify.1 new file mode 100644 index 0000000..652db95 --- /dev/null +++ b/man/mu-verify.1 @@ -0,0 +1,69 @@ +.TH MU VERIFY 1 "April 2022" "User Manuals" + +.SH NAME + +mu verify\- verify message signatures and display information about them + +.SH SYNOPSIS + +.B mu verify [options] + +.SH DESCRIPTION + +\fBmu verify\fR is the \fBmu\fR command for verifying message signatures (such +as PGP/GPG signatures) and displaying information about them. The sub-command +works on message files, and does not require the message to be indexed in the +database. + +.SH OPTIONS + +.TP +\fB\-r\fR, \fB\-\-auto\-retrieve\fR +attempt to find keys online (see the \fBauto-key-retrieve\fR option in the +\fBgnupg(1)\fR documentation). + +.SH EXAMPLES + +To display aggregated (one-line) information about the verification status in a +message: +.nf + $ mu verify msgfile +.fi + +To display information about all the signatures: +.nf + $ mu verify --verbose msgfile +.fi + +If you only want to use the exit code, you can use: +.nf + $ mu verify --quiet msgfile +.fi +which does not give any output unless there is an error. + +.SH RETURN VALUE + +\fBmu verify\fR returns 0 when all signatures could be verified to be good, and +returns some non-zero error code when this is not the case. + +When there are no signatures, returns 0 as well. + +.nf +| code | meaning | +|------+--------------------------------| +| 0 | ok | +| 1 | some non-verified signature(s) | +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1) diff --git a/man/mu-view.1 b/man/mu-view.1 new file mode 100644 index 0000000..8dc746d --- /dev/null +++ b/man/mu-view.1 @@ -0,0 +1,53 @@ +.TH MU VIEW 1 "April 2022" "User Manuals" + +.SH NAME + +mu view\- display an e-mail message file + +.SH SYNOPSIS + +.B mu view [options] [] + +.SH DESCRIPTION + +\fBmu view\fR is the \fBmu\fR command for displaying e-mail message files. It +works on message files and does \fInot\fR require the message to be indexed in +the database. + +The command shows some common headers (From:, To:, Cc:, Bcc:, Subject: and +Date:), the list of attachments and the plain-text body of the message (if +any). + +.SH OPTIONS + +.TP +\fB\-\-summary-len\fR=\fI\fR +instead of displaying the full message, output a summary based upon the first +\fI\fR lines of the message. + +.TP +\fB\-\-terminate\fR +terminate messages with \\f (\fIform-feed\fR) characters when displaying +them. This is useful when you want to further process them. + +.TP +\fB\-\-decrypt\fR +attempt to decrypt encrypted message bodies. This is only possible if \fBmu\fR +was built with crypto-support. + +.TP +\fB\-\-auto-retrieve\fR +attempt to retrieve crypto-keys automatically from the network, when needed. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1) diff --git a/man/mu.1 b/man/mu.1 new file mode 100644 index 0000000..95469e9 --- /dev/null +++ b/man/mu.1 @@ -0,0 +1,188 @@ +.TH MU 1 "May 2022" "User Manuals" + +.SH NAME + +mu \- a set of tools to deal with Maildirs and message files, in particular to +index and search e-mail messages. + +.SH SYNOPSIS + +In alphabetical order: + +.B mu [options] +general mu command. + +.B mu add +add specific messages to the database. See +.BR mu-add(1) + +.B mu cfind [options] [] +find contacts. See +.BR mu-cfind(1) + +.B mu extract [options] [] [] +extract attachments and other MIME-parts. See +.BR mu-extract(1) + +.B mu find [options] +find messages. See +.BR mu-find(1) + +.B mu help [command] +get help for some command. See +.BR mu-help(1) + +.B mu index [options] +(re)index the messages in a Maildir. See +.BR mu-index(1) + +.B mu info [options] +show information about the mu database +.BR mu-info(1) + +.B mu init [options] +initialize the mu database +.BR mu-init(1) + +.B mu mkdir [options] [] +create a new Maildir. See +.BR mu-mkdir(1) + +.B mu remove [options] +remove specific messages from the database. See +.BR mu-remove(1) + +.B mu script [options] +run a mu (Guile) script. See +.BR mu-script(1) + +.B mu server [options] +start a server process (for \fBmu4e\fR-internal use). See +.BR mu-server(1) + +.B mu view [] +view a specific message. See +.BR mu-view(1) + +.SH DESCRIPTION + +\fBmu\fR is a set of tools for dealing with Maildirs and the e-mail messages +in them. + +\fBmu\fR's main purpose is to enable searching of e-mail messages. It +does so by periodically scanning a Maildir directory tree and +analyzing the e-mail messages found (this is called 'indexing'). The +results of this analysis are stored in a database, which can then be +queried. + +In addition to indexing and searching, \fBmu\fR also offers +functionality for viewing messages, extracting attachments and +creating maildirs, and searching and exporting contact information. + +\fBmu\fR can be used from the command line or can be integrated with various +e-mail clients. + +This manpage gives a general overview of the available commands +(\fBindex\fR, \fBfind\fR, etc.); each \fBmu\fR command has its own +man-page as well. + +.SH COLORS + +Some \fBmu\fR sub-commands support colorized output, and do so by +default. If you don't want colors, you can use \fI--nocolor\fR. + +Currently, \fBmu find\fR, \fBmu view\fR, \fBmu cfind\fR and \fBmu extract\fR +support colors. + +.SH ENCODING + +\fBmu\fR's output is in the current locale, with the exceptions of the output +specifically meant for output to UTF8-encoded files. In practice, this means +that the output of commands \fBindex\fR, \fBview\fR, +\fBextract\fR is always encoded according to the current locale. + +The same is true for \fBfind\fR and \fBcfind\fR, with some exceptions, where +the output is always UTF-8, regardless of the locale. + +For \fBcfind\fR the exception is \fI--format=bbdb\fR. This is hard-coded to +UTF-8, and as such specified in the output-file, so emacs/bbdb can handle it +correctly without guessing. + +For \fBfind\fR the output is encoded according the locale for +\fI--format=plain\fR (the default), and UTF-8 for all other formats +(\fIjson\fR, \fIsexp\fR, \fIxml\fR). + +.SH DATABASE AND FILE + +Commands \fBmu index\fR and \fBfind\fR and \fBcfind\fR work with the database, +while the other ones work on individual mail files. Hence, running \fBview\fR, +\fBmkdir\fR and \fBextract\fR does not require the mu database. + +The various commands are discussed in more detail in their own separate +man-pages; here the general options are discussed. + +.SH OPTIONS + +\fBmu\fR offers several general options that apply to all commands, +including \fBmu\fR without any command. + +.TP +\fB\-\-muhome\fR +use an alternative directory to store and read the database, write the logs, +etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux +by default \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of \fBmu\fR defaulted +to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR. + +.TP +\fB\-d\fR, \fB\-\-debug\fR +makes \fBmu\fR generate extra debug information, +useful for debugging the program itself. By default, debug information goes to +the log file, \fI~/.cache/mu/mu.log\fR. It can safely be deleted when \fBmu\fR is +not running. When running with \fB--debug\fR option, the log file can grow +rather quickly. See the note on logging below. + +.TP +\fB\-q\fR, \fB\-\-quiet\fR +causes \fBmu\fR not to output informational +messages and progress information to standard output, but only to the log +file. Error messages will still be sent to standard error. Note that \fBmu +index\fR is \fBmuch\fR faster with \fB\-\-quiet\fR, so it is recommended you +use this option when using \fBmu\fR from scripts etc. + +.TP +\fB\-\-log-stderr\fR +causes \fBmu\fR to \fBnot\fR output log messages to standard error, in +addition to sending them to the log file. + +.TP +\fB\-V\fR, \fB\-\-version\fR +prints \fBmu\fR version and copyright information. + +.TP +\fB\-h\fR, \fB\-\-help\fR +lists the various command line options. + +.SH ENVIRONMENT + +\fBMUHOME\fR can be used as an alternative to \fB\-\-muhome\fR. The latter has precedence. + +\fBNO_COLOR\fR can be used as an alternative to \fB\-\-nocolor\fR. + +.SH ERROR CODES + +The various mu subcommands typically exit with 0 (zero) upon success, and +non-zero when some error occurred. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" +.BR mu-index (1), mu-find (1), mu-cfind (1), mu-mkdir (1), mu-view (1), +.BR mu-extract (1), mu-easy (1), mu-bookmarks (5), mu-query (7) +.BR https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..3d2839a --- /dev/null +++ b/meson.build @@ -0,0 +1,212 @@ +## Copyright (C) 2022-2023 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +################################################################################ +# project setup +# +project('mu', ['c', 'cpp'], + version: '1.8.14', + meson_version: '>= 0.52.0', # debian 10 + license: 'GPL-3.0-or-later', + default_options : [ + 'buildtype=debugoptimized', + 'warning_level=3', + 'c_std=c11', + 'cpp_std=c++17' + ] + ) + +# installation paths +prefixdir = get_option('prefix') +bindir = prefixdir / get_option('bindir') +datadir = prefixdir / get_option('datadir') +mandir = prefixdir / get_option('mandir') +infodir = prefixdir / get_option('infodir') + +# allow for configuring lispdir, as with autotools. +# default to

= 2.58') +gobject_dep = dependency('gobject-2.0', version: '>= 2.58') +gio_dep = dependency('gio-2.0', version: '>= 2.50') +gmime_dep = dependency('gmime-3.0', version: '>= 3.2') +xapian_dep = dependency('xapian-core', version:'>= 1.4') +thread_dep = dependency('threads') + +awk=find_program(['gawk', 'awk']) +gzip=find_program('gzip') + +# soft dependencies +guile_dep = dependency('guile-3.0', required: get_option('guile')) +# soft dependencies + +# emacs -- needed for mu4e compilation +emacs_name=get_option('emacs') +emacs=find_program([emacs_name], version: '>=25.3', required:false) +if not emacs.found() + message('emacs not found; not pre-compiling mu4e sources') +endif + +makeinfo=find_program(['makeinfo'], required:false) +if not makeinfo.found() + message('makeinfo (texinfo) not found; not building info documentation') +else + install_info=find_program(['install-info'], required:false) + if not install_info.found() + message('install-info not found') + else + install_info_script=join_paths(meson.current_source_dir(), 'build-aux', + 'meson-install-info.sh') + endif +endif + +# readline. annoyingly, macos has an incompatible libedit claiming to be +# readline. this is only a dev/debug convenience for the mu4e repl. +readline_dep=[] +if get_option('readline').enabled() + readline_dep = dependency('readline', version:'>= 8.0') + config_h_data.set('HAVE_LIBREADLINE', 1) + config_h_data.set('HAVE_READLINE_READLINE_H', 1) + config_h_data.set('HAVE_READLINE_HISTORY', 1) + config_h_data.set('HAVE_READLINE_HISTORY_H', 1) +endif + + +################################################################################ +# write out version.texi (for texiinfo builds in mu4e, guile) +version_texi_data=configuration_data() +version_texi_data.set('VERSION', meson.project_version()) +version_texi_data.set('EDITION', meson.project_version()) +version_texi_data.set('UPDATED', + run_command('date', '+%d %B %Y', check:true).stdout().strip()) +version_texi_data.set('UPDATEDMONTH', + run_command('date', '+%B %Y', check:true).stdout().strip()) + +configure_file(input: 'version.texi.in', + output: 'version.texi', + configuration: version_texi_data) + +################################################################################ +# install some data files +install_data('NEWS.org', + install_dir : join_paths(datadir,'doc', 'mu')) + +################################################################################ +# subdirs +subdir('lib') +subdir('mu') +subdir('man') + +if emacs.found() + subdir('mu4e') +endif + +if not get_option('guile').disabled() and guile_dep.found() + config_h_data.set('BUILD_GUILE', 1) + config_h_data.set_quoted('GUILE_BINARY', + guile_dep.get_pkgconfig_variable('guile')) + #message('guile is disabled for now') + subdir('guile') +endif + +config_h_data.set_quoted('MU_PROGRAM', mu.full_path()) +################################################################################ + +################################################################################ +# write-out config.h +configure_file(output : 'config.h', configuration : config_h_data) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..587b479 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,35 @@ +## Copyright (C) 2022 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +option('guile', + type : 'feature', + value: 'auto', + description: 'build the guile scripting support (requires guile-3.x)') + +option('readline', + type: 'feature', + value: 'auto', + description: 'enable readline support for the mu4e repl') + +option('emacs', + type: 'string', + value: 'emacs', + description: 'name/path of the emacs executable') + +option('lispdir', + type: 'string', + description: 'path under which to install emacs-lisp files') diff --git a/mu/Makefile.am b/mu/Makefile.am new file mode 100644 index 0000000..e9f6f6e --- /dev/null +++ b/mu/Makefile.am @@ -0,0 +1,74 @@ +## Copyright (C) 2010-2020 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CPPFLAGS= \ + -I${top_srcdir}/lib \ + $(GLIB_CFLAGS) \ + $(XAPIAN_CFLAGS) \ + $(CODE_COVERAGE_CFLAGS) + +AM_CXXFLAGS= \ + $(GMIME_CFLAGS) \ + -DMU_SCRIPTS_DIR="\"$(pkgdatadir)/scripts/\"" \ + $(ASAN_CXXCFLAGS) \ + $(WARN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -Wno-switch-enum + +AM_LDFLAGS= \ + $(ASAN_LDFLAGS) + +bin_PROGRAMS= \ + mu + +# note, mu.cc is only '.cc' and not '.c' because libmu must explicitly +# be linked as c++, not c. +mu_SOURCES= \ + mu.cc \ + mu-cmd-cfind.cc \ + mu-config.cc \ + mu-config.hh \ + mu-cmd-extract.cc \ + mu-cmd-find.cc \ + mu-cmd-index.cc \ + mu-cmd-server.cc \ + mu-cmd-script.cc \ + mu-cmd-fields.cc \ + mu-cmd.cc \ + mu-cmd.hh + +BUILT_SOURCES= \ + mu-help-strings.inc + +mu-help-strings.inc: mu-help-strings.txt mu-help-strings.awk + $(AM_V_GEN) $(AWK) -f ${top_srcdir}/mu/mu-help-strings.awk < $< > $@ + +mu_LDADD= \ + ${top_builddir}/lib/libmu.la \ + ${top_builddir}/lib/utils/libmu-utils.la \ + $(GLIB_LIBS) \ + $(XAPIAN_LIBS) \ + $(READLINE_LIBS) \ + $(CODE_COVERAGE_LIBS) + +EXTRA_DIST= \ + mu-help-strings.awk \ + mu-help-strings.txt + +CLEANFILES= \ + $(BUILT_SOURCES) diff --git a/mu/meson.build b/mu/meson.build new file mode 100644 index 0000000..99659ea --- /dev/null +++ b/mu/meson.build @@ -0,0 +1,43 @@ +## Copyright (C) 2021 Dirk-Jan C. Binnema +## +## 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, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +awk_script=join_paths(meson.current_source_dir(), 'mu-help-strings.awk') +mu_help_strings_h=custom_target('mu_help', + input: 'mu-help-strings.txt', + output: 'mu-help-strings.inc', + command: [awk, '-f', awk_script, '@INPUT@'], + capture: true) +mu = executable( + 'mu', [ + 'mu.cc', + 'mu-cmd-cfind.cc', + 'mu-cmd-extract.cc', + 'mu-cmd-fields.cc', + 'mu-cmd-find.cc', + 'mu-cmd-index.cc', + 'mu-cmd-script.cc', + 'mu-cmd-server.cc', + 'mu-cmd.cc', + 'mu-cmd.hh', + 'mu-config.cc', + 'mu-config.hh', + mu_help_strings_h +], + dependencies: [ glib_dep, gmime_dep, lib_mu_dep, thread_dep, config_h_dep ], + cpp_args: ['-DMU_SCRIPTS_DIR="'+ join_paths(datadir, 'mu', 'scripts') + '"'], + install: true) + +subdir('tests') diff --git a/mu/mu-cmd-cfind.cc b/mu/mu-cmd-cfind.cc new file mode 100644 index 0000000..893ce84 --- /dev/null +++ b/mu/mu-cmd-cfind.cc @@ -0,0 +1,439 @@ +/* +** Copyright (C) 2011-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, Inc., 51 +** Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include + +#include +#include +#include +#include + +#include "mu-cmd.hh" +#include "mu-contacts-cache.hh" +#include "mu-runtime.hh" + +#include "utils/mu-util.h" +#include "utils/mu-utils.hh" +#include "utils/mu-error.hh" + +using namespace Mu; + +/** + * macro to check whether the string is empty, ie. if it's NULL or + * it's length is 0 + * + * @param S a string + * + * @return TRUE if the string is empty, FALSE otherwise + */ +#define mu_str_is_empty(S) ((!(S)||!(*S))?TRUE:FALSE) + + +/** + * guess the last name for the given name; clearly, + * this is just a rough guess for setting an initial value. + * + * @param name a name + * + * @return the last name, as a newly allocated string (free with + * g_free) + */ +static gchar* +guess_last_name(const char* name) +{ + const gchar* lastsp; + + if (!name) + return g_strdup(""); + + lastsp = g_strrstr(name, " "); + + return g_strdup(lastsp ? lastsp + 1 : ""); +} + +/** + * guess the first name for the given name; clearly, + * this is just a rough guess for setting an initial value. + * + * @param name a name + * + * @return the first name, as a newly allocated string (free with + * g_free) + */ +static gchar* +guess_first_name(const char* name) +{ + const gchar* lastsp; + + if (!name) + return g_strdup(""); + + lastsp = g_strrstr(name, " "); + + if (lastsp) + return g_strndup(name, lastsp - name); + else + return g_strdup(name); +} + +/** + * guess some nick name for the given name; if we can determine an + * first name, last name, the nick will be first name + the first char + * of the last name. otherwise, it's just the first name. clearly, + * this is just a rough guess for setting an initial value for nicks. + * + * @param name a name + * + * @return the guessed nick, as a newly allocated string (free with g_free) + */ +static gchar* +cleanup_str(const char* str) +{ + gchar* s; + const gchar* cur; + unsigned i; + + if (mu_str_is_empty(str)) + return g_strdup(""); + + s = g_new0(char, strlen(str) + 1); + + for (cur = str, i = 0; *cur; ++cur) { + if (ispunct(*cur) || isspace(*cur)) + continue; + else + s[i++] = *cur; + } + + return s; +} + +static char* +uniquify_nick(const char* nick, GHashTable* nicks) +{ + guint u; + + for (u = 2; u != 1000; ++u) { + char* cand; + cand = g_strdup_printf("%s%u", nick, u); + if (!g_hash_table_contains(nicks, cand)) + return cand; + } + + return g_strdup(nick); /* if all else fails */ +} + +static gchar* +guess_nick(const char* name, GHashTable* nicks) +{ + gchar *fname, *lname, *nick; + gchar initial[7]; + + fname = guess_first_name(name); + lname = guess_last_name(name); + + /* if there's no last name, use first name as the nick */ + if (mu_str_is_empty(fname) || mu_str_is_empty(lname)) { + g_free(lname); + nick = fname; + goto leave; + } + + memset(initial, 0, sizeof(initial)); + /* couldn't we get an initial for the last name? */ + if (g_unichar_to_utf8(g_utf8_get_char(lname), initial) == 0) { + g_free(lname); + nick = fname; + goto leave; + } + + nick = g_strdup_printf("%s%s", fname, initial); + g_free(fname); + g_free(lname); + +leave : { + gchar* tmp; + tmp = cleanup_str(nick); + g_free(nick); + nick = tmp; +} + + if (g_hash_table_contains(nicks, nick)) { + char* tmp; + tmp = uniquify_nick(nick, nicks); + g_free(nick); + nick = tmp; + } + + g_hash_table_add(nicks, g_strdup(nick)); + + return nick; +} + +static void +print_header(const MuConfigFormat format) +{ + switch (format) { + case MU_CONFIG_FORMAT_BBDB: + g_print(";; -*-coding: utf-8-emacs;-*-\n" + ";;; file-version: 6\n"); + break; + case MU_CONFIG_FORMAT_MUTT_AB: + g_print("Matching addresses in the mu database:\n"); + break; + default: + break; + } +} + +static void +each_contact_bbdb(const std::string& email, const std::string& name, time_t tstamp) +{ + char *fname, *lname; + + fname = guess_first_name(name.c_str()); + lname = guess_last_name(name.c_str()); + + const auto now{time_to_string("%Y-%m-%d", time(NULL))}; + const auto timestamp{time_to_string("%Y-%m-%d", tstamp)}; + + g_print("[\"%s\" \"%s\" nil nil nil nil (\"%s\") " + "((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n", + fname, + lname, + email.c_str(), + now.c_str(), + timestamp.c_str()); + + g_free(fname); + g_free(lname); +} + +static void +each_contact_mutt_alias(const std::string& email, + const std::string& name, + GHashTable* nicks) +{ + if (name.empty()) + return; + + char* nick = guess_nick(name.c_str(), nicks); + mu_util_print_encoded("alias %s %s <%s>\n", nick, name.c_str(), email.c_str()); + + g_free(nick); +} + +static void +each_contact_wl(const std::string& email, + const std::string& name, + GHashTable* nicks) +{ + if (name.empty()) + return; + + char* nick = guess_nick(name.c_str(), nicks); + mu_util_print_encoded("%s \"%s\" \"%s\"\n", email.c_str(), nick, name.c_str()); + g_free(nick); +} + +static void +print_plain(const std::string& email, const std::string& name, bool color) +{ + if (!name.empty()) { + if (color) + ::fputs(MU_COLOR_MAGENTA, stdout); + mu_util_fputs_encoded(name.c_str(), stdout); + ::fputs(" ", stdout); + } + + if (color) + ::fputs(MU_COLOR_GREEN, stdout); + + mu_util_fputs_encoded(email.c_str(), stdout); + + if (color) + fputs(MU_COLOR_DEFAULT, stdout); + + fputs("\n", stdout); +} + +struct ECData { + MuConfigFormat format; + gboolean color, personal; + time_t after; + GRegex* rx; + GHashTable* nicks; + size_t maxnum; + size_t n; +}; + +static void +each_contact(const Mu::Contact& ci, ECData& ecdata) +{ + if (ecdata.personal && !ci.personal) + return; + + if (ci.message_date < ecdata.after) + return; + + if (ecdata.rx && + !g_regex_match(ecdata.rx, ci.email.c_str(), (GRegexMatchFlags)0, NULL) && + !g_regex_match(ecdata.rx, + ci.name.empty() ? "" : ci.name.c_str(), + (GRegexMatchFlags)0, + NULL)) + return; + + ++ecdata.n; + + switch (ecdata.format) { + case MU_CONFIG_FORMAT_MUTT_ALIAS: + each_contact_mutt_alias(ci.email, ci.name, ecdata.nicks); + break; + case MU_CONFIG_FORMAT_MUTT_AB: + mu_util_print_encoded("%s\t%s\t\n", ci.email.c_str(), ci.name.c_str()); + break; + case MU_CONFIG_FORMAT_WL: each_contact_wl(ci.email, ci.name, ecdata.nicks); + break; + case MU_CONFIG_FORMAT_ORG_CONTACT: + if (!ci.name.empty()) + mu_util_print_encoded("* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n", + ci.name.c_str(), + ci.email.c_str()); + break; + case MU_CONFIG_FORMAT_BBDB: each_contact_bbdb(ci.email, ci.name, ci.message_date); + break; + case MU_CONFIG_FORMAT_CSV: + mu_util_print_encoded("%s,%s\n", + ci.name.empty() ? "" : Mu::quote(ci.name).c_str(), + Mu::quote(ci.email).c_str()); + break; + case MU_CONFIG_FORMAT_DEBUG: { + char datebuf[32]; + const auto mdate(static_cast<::time_t>(ci.message_date)); + ::strftime(datebuf, sizeof(datebuf), "%F %T", ::gmtime(&mdate)); + g_print("%s\n\tname: %s\n\t%s\n\tpersonal: %s\n\tfreq: %zu\n" + "\tlast-seen: %s\n", + ci.email.c_str(), + ci.name.empty() ? "" : ci.name.c_str(), + ci.display_name(true).c_str(), + ci.personal ? "yes" : "no", + ci.frequency, + datebuf); + } + break; + default: + print_plain(ci.email, ci.name, ecdata.color); + } +} + +static Result +run_cmd_cfind(const Mu::Store& store, + const char* pattern, + gboolean personal, + time_t after, + int maxnum, + const MuConfigFormat format, + gboolean color) +{ + ECData ecdata{}; + GError *err{}; + + memset(&ecdata, 0, sizeof(ecdata)); + + if (pattern) { + ecdata.rx = g_regex_new( + pattern, + (GRegexCompileFlags)(G_REGEX_CASELESS | G_REGEX_OPTIMIZE), + (GRegexMatchFlags)0, &err); + + if (!ecdata.rx) + return Err(Error::Code::Internal, &err, "invalid cfind regexp"); + } + + ecdata.personal = personal; + ecdata.n = 0; + ecdata.after = after; + ecdata.maxnum = maxnum; + ecdata.format = format; + ecdata.color = color; + ecdata.nicks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + print_header(format); + + store.contacts_cache().for_each([&](const auto& ci) { + each_contact(ci, ecdata); + return ecdata.maxnum == 0 || ecdata.n < ecdata.maxnum; + }); + g_hash_table_unref(ecdata.nicks); + + if (ecdata.rx) + g_regex_unref(ecdata.rx); + + if (ecdata.n == 0) + return Err(Error::Code::ContactNotFound, "no matching contacts found"); + else + return Ok(); +} + +static gboolean +cfind_params_valid(const MuConfig* opts) +{ + if (!opts || opts->cmd != MU_CONFIG_CMD_CFIND) + return FALSE; + + switch (opts->format) { + case MU_CONFIG_FORMAT_PLAIN: + case MU_CONFIG_FORMAT_MUTT_ALIAS: + case MU_CONFIG_FORMAT_MUTT_AB: + case MU_CONFIG_FORMAT_WL: + case MU_CONFIG_FORMAT_BBDB: + case MU_CONFIG_FORMAT_CSV: + case MU_CONFIG_FORMAT_ORG_CONTACT: + case MU_CONFIG_FORMAT_DEBUG: break; + default: + g_printerr("invalid output format %s\n", + opts->formatstr ? opts->formatstr : ""); + return FALSE; + } + + /* only one pattern allowed */ + if (opts->params[1] && opts->params[2]) { + g_printerr("usage: mu cfind [options] []\n"); + return FALSE; + } + + return TRUE; +} + +Result +Mu::mu_cmd_cfind(const Mu::Store& store, const MuConfig* opts) +{ + if (!cfind_params_valid(opts)) + return Err(Error::Code::InvalidArgument, "error in parameters"); + else + return run_cmd_cfind(store, + opts->params[1], + opts->personal, + opts->after, + opts->maxnum, + opts->format, + !opts->nocolor); +} diff --git a/mu/mu-cmd-extract.cc b/mu/mu-cmd-extract.cc new file mode 100644 index 0000000..d071441 --- /dev/null +++ b/mu/mu-cmd-extract.cc @@ -0,0 +1,205 @@ +/* +** Copyright (C) 2010-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" +#include "mu-cmd.hh" +#include "mu-config.hh" +#include "utils/mu-util.h" +#include "utils/mu-utils.hh" +#include +#include + +using namespace Mu; + + +static Result +save_part(const Message::Part& part, size_t idx, const MuConfig* opts) +{ + const auto targetdir = std::invoke([&]{ + auto tdir{std::string{opts->targetdir ? opts->targetdir : ""}}; + return tdir.empty() ? tdir : tdir + G_DIR_SEPARATOR_S; + }); + const auto path{targetdir + + part.cooked_filename().value_or(format("part-%zu", idx))}; + + if (auto&& res{part.to_file(path, opts->overwrite)}; !res) + return Err(res.error()); + + if (opts->play) { + GError *err{}; + if (auto res{mu_util_play(path.c_str(), &err)}; + res != MU_OK) + return Err(Error::Code::Play, &err, "playing '%s' failed", + path.c_str()); + } + + return Ok(); +} + +static Result +save_parts(const std::string& path, Option& filename_rx, + const MuConfig* opts) +{ + auto message{Message::make_from_path(path, mu_config_message_options(opts))}; + if (!message) + return Err(std::move(message.error())); + + + size_t partnum{}, saved_num{}; + const auto partnums = std::invoke([&]()->std::vector { + std::vector nums; + for (auto&& numstr : split(opts->parts ? opts->parts : "", ',')) + nums.emplace_back( + static_cast(::atoi(numstr.c_str()))); + return nums; + }); + + + for (auto&& part: message->parts()) { + + ++partnum; + + if (!opts->save_all) { + + if (!partnums.empty() && + !seq_some(partnums, [&](auto&& num){return num==partnum;})) + continue; // not a wanted partnum. + + if (filename_rx && (!part.raw_filename() || + !std::regex_match(*part.raw_filename(), + std::regex{*filename_rx}))) + continue; // not a wanted pattern. + } + + if (auto res = save_part(part, partnum, opts); !res) + return res; + + ++saved_num; + } + + // if (saved_num == 0) + // return Err(Error::Code::File, + // "no %s extracted from this message", + // opts->save_attachments ? "attachments" : "parts"); + // else + return Ok(); +} + +#define color_maybe(C) \ + do { \ + if (color) \ + fputs((C), stdout); \ + } while (0) + +static void +show_part(const MessagePart& part, size_t index, bool color) +{ + /* index */ + g_print(" %zu ", index); + + /* filename */ + color_maybe(MU_COLOR_GREEN); + const auto fname{part.raw_filename()}; + mu_util_fputs_encoded(fname ? fname->c_str() : "", stdout); + + mu_util_fputs_encoded(" ", stdout); + + /* content-type */ + color_maybe(MU_COLOR_BLUE); + const auto ctype{part.mime_type()}; + mu_util_fputs_encoded(ctype ? ctype->c_str() : "", stdout); + + /* /\* disposition *\/ */ + color_maybe(MU_COLOR_MAGENTA); + mu_util_print_encoded(" [%s]", part.is_attachment() ? + "attachment" : "inline"); + /* size */ + if (part.size() > 0) { + color_maybe(MU_COLOR_CYAN); + g_print(" (%zu bytes)", part.size()); + } + + color_maybe(MU_COLOR_DEFAULT); + fputs("\n", stdout); +} + +static Mu::Result +show_parts(const char* path, const MuConfig* opts) +{ + //msgopts = mu_config_get_msg_options(opts); + + auto msg_res{Message::make_from_path(path, mu_config_message_options(opts))}; + if (!msg_res) + return Err(std::move(msg_res.error())); + + /* TODO: update this for crypto */ + size_t index{}; + g_print("MIME-parts in this message:\n"); + for (auto&& part: msg_res->parts()) + show_part(part, ++index, !opts->nocolor); + + return Ok(); +} + +static Mu::Result +check_params(const MuConfig* opts) +{ + size_t param_num; + param_num = mu_config_param_num(opts); + + if (param_num < 2) + return Err(Error::Code::InvalidArgument, "parameters missing"); + + if (opts->save_attachments || opts->save_all) + if (opts->parts || param_num == 3) + return Err(Error::Code::User, + "--save-attachments and --save-all don't " + "accept a filename pattern or --parts"); + + if (opts->save_attachments && opts->save_all) + return Err(Error::Code::User, + "only one of --save-attachments and" + " --save-all is allowed"); + return Ok(); +} + + +Mu::Result +Mu::mu_cmd_extract(const MuConfig* opts) +{ + if (!opts || opts->cmd != MU_CONFIG_CMD_EXTRACT) + return Err(Error::Code::Internal, "error in arguments"); + if (auto res = check_params(opts); !res) + return Err(std::move(res.error())); + + if (!opts->params[2] && !opts->parts && + !opts->save_attachments && !opts->save_all) + return show_parts(opts->params[1], opts); /* show, don't save */ + + if (!mu_util_check_dir(opts->targetdir, FALSE, TRUE)) + return Err(Error::Code::File, + "target '%s' is not a writable directory", + opts->targetdir); + + Option pattern{}; + if (opts->params[2]) + pattern = opts->params[2]; + + return save_parts(opts->params[1], pattern, opts); +} diff --git a/mu/mu-cmd-fields.cc b/mu/mu-cmd-fields.cc new file mode 100644 index 0000000..729ccf4 --- /dev/null +++ b/mu/mu-cmd-fields.cc @@ -0,0 +1,151 @@ +/* +** Copyright (C) 2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, Inc., 51 +** Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include +#include + +#include "mu-cmd.hh" +#include +#include "utils/mu-utils.hh" + +#include "thirdparty/tabulate.hpp" + + +using namespace Mu; +using namespace tabulate; + + +static void +table_header(Table& table, const MuConfig* opts) +{ + if (opts->nocolor) + return; + + (*table.begin()).format() + .font_style({FontStyle::bold}) + .font_color(Color::blue); + +} + +static void +show_fields(const MuConfig* opts) +{ + using namespace std::string_literals; + + Table fields; + fields.add_row({"field-name", "alias", "short", "search", + "value", "sexp", "example query", "description"}); + + auto disp= [&](std::string_view sv)->std::string { + if (sv.empty()) + return ""; + else + return format("%*s", STR_V(sv)); + }; + + auto searchable=[&](const Field& field)->std::string { + if (field.is_boolean_term()) + return "boolean"; + if (field.is_indexable_term()) + return "index"; + if (field.is_normal_term()) + return "yes"; + if (field.is_contact()) + return "contact"; + if (field.is_range()) + return "range"; + return "no"; + }; + + size_t row{}; + field_for_each([&](auto&& field){ + if (field.is_internal()) + return; // skip. + + fields.add_row({format("%*s", STR_V(field.name)), + field.alias.empty() ? "" : format("%*s", STR_V(field.alias)), + field.shortcut ? format("%c", field.shortcut) : ""s, + searchable(field), + field.is_value() ? "yes" : "no", + field.include_in_sexp() ? "yes" : "no", + disp(field.example_query), + disp(field.description)}); + ++row; + }); + + table_header(fields, opts); + + std::cout << fields << '\n'; +} + +static void +show_flags(const MuConfig* opts) +{ + using namespace tabulate; + using namespace std::string_literals; + + Table flags; + flags.add_row({"flag", "shortcut", "category", "description"}); + + flag_infos_for_each([&](const MessageFlagInfo& info) { + + const auto catname = std::invoke( + [](MessageFlagCategory cat)->std::string { + switch(cat){ + case MessageFlagCategory::Mailfile: + return "file"; + case MessageFlagCategory::Maildir: + return "maildir"; + case MessageFlagCategory::Content: + return "content"; + case MessageFlagCategory::Pseudo: + return "pseudo"; + default: + return {}; + } + }, info.category); + + flags.add_row({format("%*s", STR_V(info.name)), + format("%c", info.shortcut), + catname, + std::string{info.description}}); + }); + + table_header(flags, opts); + + std::cout << flags << '\n'; +} + + + +Result +Mu::mu_cmd_fields(const MuConfig* opts) +{ + g_return_val_if_fail(opts, Err(Error::Code::Internal, "no opts")); + + if (!locale_workaround()) + return Err(Error::Code::User, "failed to find a working locale"); + + + std::cout << "#\n# message fields\n#\n"; + show_fields(opts); + std::cout << "\n#\n# message flags\n#\n"; + show_flags(opts); + + return Ok(); + +} diff --git a/mu/mu-cmd-find.cc b/mu/mu-cmd-find.cc new file mode 100644 index 0000000..898244f --- /dev/null +++ b/mu/mu-cmd-find.cc @@ -0,0 +1,612 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "message/mu-message.hh" +#include "mu-maildir.hh" +#include "mu-query-match-deciders.hh" +#include "mu-query.hh" +#include "mu-bookmarks.hh" +#include "mu-runtime.hh" +#include "message/mu-message.hh" + +#include "utils/mu-option.hh" +#include "utils/mu-util.h" + +#include "mu-cmd.hh" +#include "utils/mu-utils.hh" + +using namespace Mu; + +struct OutputInfo { + Xapian::docid docid{}; + bool header{}; + bool footer{}; + bool last{}; + Option match_info; +}; + +constexpr auto FirstOutput{OutputInfo{0, true, false, {}, {}}}; +constexpr auto LastOutput{OutputInfo{0, false, true, {}, {}}}; + +using OutputFunc = std::function& msg, const OutputInfo&, + const MuConfig*, GError**)>; + +static Result +print_internal(const Store& store, + const std::string& expr, + gboolean xapian, + gboolean warn) +{ + std::cout << store.parse_query(expr, xapian) << "\n"; + return Ok(); +} + +static Result +run_query(const Store& store, const std::string& expr, const MuConfig* opts) +{ + const auto sortfield{field_from_name(opts->sortfield ? opts->sortfield : "")}; + if (!sortfield && opts->sortfield) + return Err(Error::Code::InvalidArgument, + "invalid sort field: '%s'", opts->sortfield); + + Mu::QueryFlags qflags{QueryFlags::SkipUnreadable}; + if (opts->reverse) + qflags |= QueryFlags::Descending; + if (opts->skip_dups) + qflags |= QueryFlags::SkipDuplicates; + if (opts->include_related) + qflags |= QueryFlags::IncludeRelated; + if (opts->threads) + qflags |= QueryFlags::Threading; + + return store.run_query(expr, sortfield.value_or(field_from_id(Field::Id::Date)).id, + qflags, opts->maxnum); +} + +static gboolean +exec_cmd(const Option& msg, const OutputInfo& info, const MuConfig* opts, GError** err) +{ + if (!msg) + return TRUE; + + gint status; + char * cmdline, *escpath; + gboolean rv; + + escpath = g_shell_quote(msg->path().c_str()); + cmdline = g_strdup_printf("%s %s", opts->exec, escpath); + + rv = g_spawn_command_line_sync(cmdline, NULL, NULL, &status, err); + + g_free(cmdline); + g_free(escpath); + + return rv; +} + +static gchar* +resolve_bookmark(const MuConfig* opts, GError** err) +{ + MuBookmarks* bm; + char* val; + const gchar* bmfile; + + bmfile = mu_runtime_path(MU_RUNTIME_PATH_BOOKMARKS); + bm = mu_bookmarks_new(bmfile); + if (!bm) { + g_set_error(err, + MU_ERROR_DOMAIN, + MU_ERROR_FILE_CANNOT_OPEN, + "failed to open bookmarks file '%s'", + bmfile); + return FALSE; + } + + val = (gchar*)mu_bookmarks_lookup(bm, opts->bookmark); + if (!val) + g_set_error(err, + MU_ERROR_DOMAIN, + MU_ERROR_NO_MATCHES, + "bookmark '%s' not found", + opts->bookmark); + else + val = g_strdup(val); + + mu_bookmarks_destroy(bm); + return val; +} + +static Result +get_query(const MuConfig* opts) +{ + GError *err{}; + gchar *query, *bookmarkval; + + /* params[0] is 'find', actual search params start with [1] */ + if (!opts->bookmark && !opts->params[1]) + return Err(Error::Code::InvalidArgument, "error in parameters"); + + bookmarkval = {}; + if (opts->bookmark) { + bookmarkval = resolve_bookmark(opts, &err); + if (!bookmarkval) + return Err(Error::Code::Command, &err, + "failed to resolve bookmark"); + } + + query = g_strjoinv(" ", &opts->params[1]); + if (bookmarkval) { + gchar* tmp; + tmp = g_strdup_printf("%s %s", bookmarkval, query); + g_free(query); + query = tmp; + } + g_free(bookmarkval); + + return Ok(to_string_gchar(std::move(query))); +} + +static bool +prepare_links(const MuConfig* opts, GError** err) +{ + /* note, mu_maildir_mkdir simply ignores whatever part of the + * mail dir already exists */ + if (auto&& res = maildir_mkdir(opts->linksdir, 0700, true); !res) { + res.error().fill_g_error(err); + return false; + } + + if (!opts->clearlinks) + return false; + + if (auto&& res = maildir_clear_links(opts->linksdir); !res) { + res.error().fill_g_error(err); + return false; + } + + return true; +} + +static bool +output_link(const Option& msg, const OutputInfo& info, const MuConfig* opts, GError** err) +{ + if (info.header) + return prepare_links(opts, err); + else if (info.footer) + return true; + + if (auto&& res = maildir_link(msg->path(), opts->linksdir); !res) { + res.error().fill_g_error(err); + return false; + } + + return true; +} + +static void +ansi_color_maybe(Field::Id field_id, gboolean color) +{ + const char* ansi; + + if (!color) + return; /* nothing to do */ + + switch (field_id) { + case Field::Id::From: ansi = MU_COLOR_CYAN; break; + + case Field::Id::To: + case Field::Id::Cc: + case Field::Id::Bcc: ansi = MU_COLOR_BLUE; break; + case Field::Id::Subject: ansi = MU_COLOR_GREEN; break; + case Field::Id::Date: ansi = MU_COLOR_MAGENTA; break; + + default: + if (field_from_id(field_id).type != Field::Type::String) + ansi = MU_COLOR_YELLOW; + else + ansi = MU_COLOR_RED; + } + + fputs(ansi, stdout); +} + +static void +ansi_reset_maybe(Field::Id field_id, gboolean color) +{ + if (!color) + return; /* nothing to do */ + + fputs(MU_COLOR_DEFAULT, stdout); +} + +static std::string +display_field(const Message& msg, Field::Id field_id) +{ + switch (field_from_id(field_id).type) { + case Field::Type::String: + return msg.document().string_value(field_id); + case Field::Type::Integer: + if (field_id == Field::Id::Priority) { + return to_string(msg.priority()); + } else if (field_id == Field::Id::Flags) { + return to_string(msg.flags()); + } else /* as string */ + return msg.document().string_value(field_id); + case Field::Type::TimeT: + return time_to_string( + "%c", static_cast<::time_t>(msg.document().integer_value(field_id))); + case Field::Type::ByteSize: + return to_string(msg.document().integer_value(field_id)); + case Field::Type::StringList: + return join(msg.document().string_vec_value(field_id), ','); + case Field::Type::ContactList: + return to_string(msg.document().contacts_value(field_id)); + default: + g_return_val_if_reached(""); + return ""; + } +} + +static void +print_summary(const Message& msg, const MuConfig* opts) +{ + const auto body{msg.body_text()}; + if (!body) + return; + + const auto summ{to_string_opt_gchar( + mu_str_summarize(body->c_str(), + opts->summary_len))}; + + g_print("Summary: "); + mu_util_fputs_encoded(summ ? summ->c_str() : "", stdout); + g_print("\n"); +} + +static void +thread_indent(const QueryMatch& info, const MuConfig* opts) +{ + const auto is_root{any_of(info.flags & QueryMatch::Flags::Root)}; + const auto first_child{any_of(info.flags & QueryMatch::Flags::First)}; + const auto last_child{any_of(info.flags & QueryMatch::Flags::Last)}; + const auto empty_parent{any_of(info.flags & QueryMatch::Flags::Orphan)}; + const auto is_dup{any_of(info.flags & QueryMatch::Flags::Duplicate)}; + // const auto is_related{any_of(info.flags & QueryMatch::Flags::Related)}; + + /* indent */ + if (opts->debug) { + ::fputs(info.thread_path.c_str(), stdout); + ::fputs(" ", stdout); + } else + for (auto i = info.thread_level; i > 1; --i) + ::fputs(" ", stdout); + + if (!is_root) { + if (first_child) + ::fputs("\\", stdout); + else if (last_child) + ::fputs("/", stdout); + else + ::fputs(" ", stdout); + ::fputs(empty_parent ? "*> " : is_dup ? "=> " + : "-> ", + stdout); + } +} + +static void +output_plain_fields(const Message& msg, const char* fields, + gboolean color, gboolean threads) +{ + const char* myfields; + int nonempty; + + g_return_if_fail(fields); + + for (myfields = fields, nonempty = 0; *myfields; ++myfields) { + const auto field_opt{field_from_shortcut(*myfields)}; + if (!field_opt || (!field_opt->is_value() && !field_opt->is_contact())) + nonempty += printf("%c", *myfields); + + else { + ansi_color_maybe(field_opt->id, color); + nonempty += mu_util_fputs_encoded( + display_field(msg, field_opt->id).c_str(), stdout); + ansi_reset_maybe(field_opt->id, color); + } + } + + if (nonempty) + fputs("\n", stdout); +} + +static gboolean +output_plain(const Option& msg, const OutputInfo& info, + const MuConfig* opts, GError** err) +{ + if (!msg) + return true; + + /* we reuse the color (whatever that may be) + * for message-priority for threads, too */ + ansi_color_maybe(Field::Id::Priority, !opts->nocolor); + if (opts->threads && info.match_info) + thread_indent(*info.match_info, opts); + + output_plain_fields(*msg, opts->fields, !opts->nocolor, opts->threads); + + if (opts->summary_len > 0) + print_summary(*msg, opts); + + return TRUE; +} + +static bool +output_sexp(const Option& msg, const OutputInfo& info, const MuConfig* opts, GError** err) +{ + if (msg) { + + if (const auto sexp{msg->cached_sexp()}; !sexp.empty()) + fputs(sexp.c_str(), stdout); + else + fputs(msg->to_sexp().to_sexp_string().c_str(), stdout); + + fputs("\n", stdout); + } + + return true; +} + +static bool +output_json(const Option& msg, const OutputInfo& info, const MuConfig* opts, GError** err) +{ + if (info.header) { + g_print("[\n"); + return true; + } + + if (info.footer) { + g_print("]\n"); + return true; + } + + if (!msg) + return true; + + g_print("%s%s\n", + msg->to_sexp().to_json_string().c_str(), + info.last ? "" : ","); + + return true; +} + +static void +print_attr_xml(const std::string& elm, const std::string& str) +{ + if (str.empty()) + return; /* empty: don't include */ + + auto&& esc{to_string_opt_gchar(g_markup_escape_text(str.c_str(), -1))}; + g_print("\t\t<%s>%s\n", elm.c_str(), esc.value_or("").c_str(), elm.c_str()); +} + +static bool +output_xml(const Option& msg, const OutputInfo& info, const MuConfig* opts, GError** err) +{ + if (info.header) { + g_print("\n"); + g_print("\n"); + return true; + } + + if (info.footer) { + g_print("\n"); + return true; + } + + g_print("\t\n"); + print_attr_xml("from", to_string(msg->from())); + print_attr_xml("to", to_string(msg->to())); + print_attr_xml("cc", to_string(msg->cc())); + print_attr_xml("subject", msg->subject()); + g_print("\t\t%u\n", (unsigned)msg->date()); + g_print("\t\t%u\n", (unsigned)msg->size()); + print_attr_xml("msgid", msg->message_id()); + print_attr_xml("path", msg->path()); + print_attr_xml("maildir", msg->maildir()); + g_print("\t\n"); + + return true; +} + +static OutputFunc +get_output_func(const MuConfig* opts, GError** err) +{ + switch (opts->format) { + case MU_CONFIG_FORMAT_LINKS: return output_link; + case MU_CONFIG_FORMAT_EXEC: return exec_cmd; + case MU_CONFIG_FORMAT_PLAIN: return output_plain; + case MU_CONFIG_FORMAT_XML: return output_xml; + case MU_CONFIG_FORMAT_SEXP: return output_sexp; + case MU_CONFIG_FORMAT_JSON: return output_json; + + default: g_return_val_if_reached(NULL); return NULL; + } +} + +static Result +output_query_results(const QueryResults& qres, const MuConfig* opts) +{ + GError* err{}; + const auto output_func{get_output_func(opts, &err)}; + if (!output_func) + return Err(Error::Code::Query, &err, "failed to find output function"); + + gboolean rv{true}; + output_func(Nothing, FirstOutput, opts, {}); + + size_t n{0}; + for (auto&& item : qres) { + n++; + auto msg{item.message()}; + if (!msg) + continue; + + if (opts->after != 0 && msg->changed() < opts->after) + continue; + + rv = output_func(msg, + {item.doc_id(), + false, + false, + n == qres.size(), /* last? */ + item.query_match()}, + opts, + &err); + if (!rv) + break; + } + output_func(Nothing, LastOutput, opts, {}); + + + if (rv) + return Ok(); + else + return Err(Error::Code::Query, &err, "error in query results output"); +} + +static Result +process_query(const Store& store, const std::string& expr, const MuConfig* opts) +{ + auto qres{run_query(store, expr, opts)}; + if (!qres) + return Err(qres.error()); + + if (qres->empty()) + return Err(Error::Code::NoMatches, "no matches for search expression"); + + return output_query_results(*qres, opts); +} + +static Result +execute_find(const Store& store, const MuConfig* opts) +{ + auto expr{get_query(opts)}; + if (!expr) + return Err(expr.error()); + + if (opts->format == MU_CONFIG_FORMAT_XQUERY) + return print_internal(store, *expr, TRUE, FALSE); + else if (opts->format == MU_CONFIG_FORMAT_MQUERY) + return print_internal(store, *expr, FALSE, opts->verbose); + else + return process_query(store, *expr, opts); +} + +static gboolean +format_params_valid(const MuConfig* opts, GError** err) +{ + switch (opts->format) { + case MU_CONFIG_FORMAT_EXEC: break; + case MU_CONFIG_FORMAT_PLAIN: + case MU_CONFIG_FORMAT_SEXP: + case MU_CONFIG_FORMAT_JSON: + case MU_CONFIG_FORMAT_LINKS: + case MU_CONFIG_FORMAT_XML: + case MU_CONFIG_FORMAT_XQUERY: + case MU_CONFIG_FORMAT_MQUERY: + if (opts->exec) { + mu_util_g_set_error(err, + MU_ERROR_IN_PARAMETERS, + "--exec and --format cannot be combined"); + return FALSE; + } + break; + default: + mu_util_g_set_error(err, + MU_ERROR_IN_PARAMETERS, + "invalid output format %s", + opts->formatstr ? opts->formatstr : ""); + return FALSE; + } + + if (opts->format == MU_CONFIG_FORMAT_LINKS && !opts->linksdir) { + mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "missing --linksdir argument"); + return FALSE; + } + + if (opts->linksdir && opts->format != MU_CONFIG_FORMAT_LINKS) { + mu_util_g_set_error(err, + MU_ERROR_IN_PARAMETERS, + "--linksdir is only valid with --format=links"); + return FALSE; + } + + return TRUE; +} + +static gboolean +query_params_valid(const MuConfig* opts, GError** err) +{ + const gchar* xpath; + + if (!opts->params[1]) { + mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "missing query"); + return FALSE; + } + + xpath = mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB); + if (mu_util_check_dir(xpath, TRUE, FALSE)) + return TRUE; + + mu_util_g_set_error(err, + MU_ERROR_FILE_CANNOT_READ, + "'%s' is not a readable Xapian directory", + xpath); + return FALSE; +} + +Result +Mu::mu_cmd_find(const Store& store, const MuConfig* opts) +{ + g_return_val_if_fail(opts, Err(Error::Code::Internal, "no opts")); + g_return_val_if_fail(opts->cmd == MU_CONFIG_CMD_FIND, Err(Error::Code::Internal, + "wrong command")); + MuConfig myopts{*opts}; + + if (myopts.exec) + myopts.format = MU_CONFIG_FORMAT_EXEC; /* pseudo format */ + + GError *err{}; + if (!query_params_valid(&myopts, &err) || !format_params_valid(&myopts, &err)) + return Err(Error::Code::InvalidArgument, &err, "invalid argument"); + else + return execute_find(store, &myopts); +} diff --git a/mu/mu-cmd-index.cc b/mu/mu-cmd-index.cc new file mode 100644 index 0000000..1559cd6 --- /dev/null +++ b/mu/mu-cmd-index.cc @@ -0,0 +1,137 @@ +/* +** Copyright (C) 2008-2022 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" +#include "mu-cmd.hh" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "index/mu-indexer.hh" +#include "mu-store.hh" +#include "mu-runtime.hh" + +#include "utils/mu-util.h" + +using namespace Mu; + +static std::atomic caught_signal; + +static void +sig_handler(int _sig) +{ + caught_signal = true; +} + +static void +install_sig_handler(void) +{ + struct sigaction action; + int i, sigs[] = {SIGINT, SIGHUP, SIGTERM}; + + sigemptyset(&action.sa_mask); + action.sa_flags = SA_RESETHAND; + action.sa_handler = sig_handler; + + for (i = 0; i != G_N_ELEMENTS(sigs); ++i) + if (sigaction(sigs[i], &action, NULL) != 0) + g_critical("set sigaction for %d failed: %s", + sigs[i], g_strerror(errno)); +} + +static void +print_stats(const Indexer::Progress& stats, bool color) +{ + const char* kars = "-\\|/"; + static auto i = 0U; + + MaybeAnsi col{color}; + using Color = MaybeAnsi::Color; + + std::cout << col.fg(Color::Yellow) << kars[++i % 4] << col.reset() << " indexing messages; " + << "checked: " << col.fg(Color::Green) << stats.checked << col.reset() + << "; updated/new: " << col.fg(Color::Green) << stats.updated << col.reset() + << "; cleaned-up: " << col.fg(Color::Green) << stats.removed << col.reset(); +} + +Result +Mu::mu_cmd_index(Mu::Store& store, const MuConfig* opts) +{ + if (!opts || opts->cmd != MU_CONFIG_CMD_INDEX || opts->params[1]) + return Err(Error::Code::InvalidArgument, "error in parameters"); + + if (opts->max_msg_size < 0) + return Err(Error::Code::InvalidArgument, + "the maximum message size must be >= 0"); + + const auto mdir{store.properties().root_maildir}; + if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0)) + return Err(Error::Code::File, "'%s' is not readable: %s", + mdir.c_str(), g_strerror(errno)); + + MaybeAnsi col{!opts->nocolor}; + using Color = MaybeAnsi::Color; + if (!opts->quiet) { + if (opts->lazycheck) + std::cout << "lazily "; + + std::cout << "indexing maildir " << col.fg(Color::Green) + << store.properties().root_maildir << col.reset() << " -> store " + << col.fg(Color::Green) << store.properties().database_path << col.reset() + << std::endl; + } + + Mu::Indexer::Config conf{}; + conf.cleanup = !opts->nocleanup; + conf.lazy_check = opts->lazycheck; + // ignore .noupdate with an empty store. + conf.ignore_noupdate = store.empty(); + + install_sig_handler(); + + auto& indexer{store.indexer()}; + indexer.start(conf); + while (!caught_signal && indexer.is_running()) { + if (!opts->quiet) + print_stats(indexer.progress(), !opts->nocolor); + + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + if (!opts->quiet) { + std::cout << "\r"; + std::cout.flush(); + } + } + + store.indexer().stop(); + + if (!opts->quiet) { + print_stats(store.indexer().progress(), !opts->nocolor); + std::cout << std::endl; + } + + return Ok(); +} diff --git a/mu/mu-cmd-script.cc b/mu/mu-cmd-script.cc new file mode 100644 index 0000000..62ef9c1 --- /dev/null +++ b/mu/mu-cmd-script.cc @@ -0,0 +1,200 @@ +/* +** Copyright (C) 2012-2020 Dirk-Jan C. Binnema +** +** 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, 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, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include +#include +#include +#include + +#include "mu-cmd.hh" +#include "mu-script.hh" +#include "mu-runtime.hh" + +#include "utils/mu-util.h" + +#define MU_GUILE_EXT ".scm" +#define MU_GUILE_DESCR_PREFIX ";; INFO: " + +#define COL(C) ((color) ? C : "") + +using namespace Mu; + +static void +print_script(const char* name, + const char* oneline, + const char* descr, + gboolean color, + gboolean verbose) +{ + g_print("%s%s%s%s%s%s%s%s", + verbose ? "\n" : " * ", + COL(MU_COLOR_GREEN), + name, + COL(MU_COLOR_DEFAULT), + oneline ? ": " : "", + COL(MU_COLOR_BLUE), + oneline ? oneline : "", + MU_COLOR_DEFAULT); + + if (verbose && descr) + g_print("%s%s%s", COL(MU_COLOR_MAGENTA), descr, COL(MU_COLOR_DEFAULT)); +} + +static gboolean +print_scripts(GSList* scripts, gboolean color, gboolean verbose, const char* rxstr, GError** err) +{ + GSList* cur; + const char* verb; + + if (!scripts) { + g_print("No scripts available\n"); + return TRUE; /* not an error */ + } + + verb = verbose ? "" : " (use --verbose for details)"; + + if (rxstr) + g_print("Available scripts matching '%s'%s:\n", rxstr, verb); + else + g_print("Available scripts%s:\n", verb); + + for (cur = scripts; cur; cur = g_slist_next(cur)) { + MuScriptInfo* msi; + const char * descr, *oneline, *name; + + msi = (MuScriptInfo*)cur->data; + name = mu_script_info_name(msi); + oneline = mu_script_info_one_line(msi); + descr = mu_script_info_description(msi); + + /* if rxstr is provide, only consider matching scriptinfos */ + if (rxstr && !mu_script_info_matches_regex(msi, rxstr, err)) { + if (err && *err) + return FALSE; + continue; + } + + print_script(name, oneline, descr, color, verbose); + } + + return TRUE; +} + +static char* +get_userpath(const char* muhome) +{ + if (muhome) + return g_build_path(G_DIR_SEPARATOR_S, muhome, "scripts", NULL); + else + return g_build_path(G_DIR_SEPARATOR_S, + g_get_user_data_dir(), + "mu", + "scripts", + NULL); +} + +static GSList* +get_script_info_list(const char* muhome, GError** err) +{ + GSList *scripts, *userscripts, *last; + char* userpath; + + scripts = mu_script_get_script_info_list(MU_SCRIPTS_DIR, + MU_GUILE_EXT, + MU_GUILE_DESCR_PREFIX, + err); + + if (err && *err) + return NULL; + + userpath = get_userpath(muhome); + + /* is there are userdir for scripts? */ + if (!mu_util_check_dir(userpath, TRUE, FALSE)) { + g_free(userpath); + return scripts; + } + + /* append it to the list we already have */ + userscripts = + mu_script_get_script_info_list(userpath, MU_GUILE_EXT, MU_GUILE_DESCR_PREFIX, err); + g_free(userpath); + + /* some error, return nothing */ + if (err && *err) { + mu_script_info_list_destroy(userscripts); + mu_script_info_list_destroy(scripts); + return NULL; + } + + /* append the user scripts */ + last = g_slist_last(scripts); + if (last) { + last->next = userscripts; + return scripts; + } else + return userscripts; /* apparently, scripts was NULL */ +} + +Mu::Result +Mu::mu_cmd_script(const MuConfig* opts) +{ + GError *err{}; + MuScriptInfo* msi; + GSList* scripts; + + if (!mu_util_supports(MU_FEATURE_GUILE)) + return Err(Error::Code::InvalidArgument, + "