From 973aec43ea54bbf95b64fbcb636403401d1ca60e Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Fri, 28 Aug 2026 18:03:05 +0200 Subject: Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14 --- .dir-locals.el | 5 + .envrc | 3 + .gitignore | 6 + LICENSE | 661 ++++++++++++++++++++++++++++ flake.lock | 59 +++ flake.nix | 57 +++ links.org | 4 + server/.gitignore | 7 + server/CMakeLists.txt | 86 ++++ server/CMakePresets.json | 29 ++ server/README | 16 + server/ct.sh | 1 + server/formal/.envrc | 1 + server/formal/.gitignore | 17 + server/formal/Makefile | 55 +++ server/formal/_CoqProject | 5 + server/formal/flake.lock | 61 +++ server/formal/flake.nix | 26 ++ server/formal/period.v | 828 +++++++++++++++++++++++++++++++++++ server/formal/period_seq.v | 834 ++++++++++++++++++++++++++++++++++++ server/formal/util.v | 92 ++++ server/hack/rwgps-get-auth-token.sh | 14 + server/locale/.gitignore | 1 + server/locale/README | 9 + server/locale/dev.sh | 7 + server/locale/en_US.po | 64 +++ server/locale/nl.po | 64 +++ server/locale/xget.sh | 13 + server/migrations/1_init_down.sql | 1 + server/migrations/1_init_up.sql | 7 + server/migrations/2_kaas_up.sql | 23 + server/routemon.pot | 58 +++ server/src/api.cpp | 206 +++++++++ server/src/api.cppm | 79 ++++ server/src/config.cpp | 167 ++++++++ server/src/config.cppm | 39 ++ server/src/database.cppm | 37 ++ server/src/datex2.cppm | 296 +++++++++++++ server/src/geo.cppm | 40 ++ server/src/gpx.cpp | 189 ++++++++ server/src/gpx.cppm | 43 ++ server/src/http_client.cppm | 62 +++ server/src/http_common.cppm | 139 ++++++ server/src/http_server.cppm | 661 ++++++++++++++++++++++++++++ server/src/locale.cppm | 245 +++++++++++ server/src/log.cppm | 148 +++++++ server/src/main.cpp | 109 +++++ server/src/problem.cppm | 60 +++ server/src/req_ctx.cppm | 51 +++ server/src/routemon.cppm | 11 + server/src/rwgps.cppm | 137 ++++++ server/src/sqlite3.cppm | 257 +++++++++++ server/src/srv.cppm | 221 ++++++++++ server/src/time.cppm | 205 +++++++++ server/src/trace.cppm | 79 ++++ server/src/util.cppm | 254 +++++++++++ server/src/xml.cpp | 61 +++ server/src/xml.cppm | 632 +++++++++++++++++++++++++++ web/index.html | 52 +++ web/script.js | 149 +++++++ web/style.css | 62 +++ 61 files changed, 7805 insertions(+) create mode 100644 .dir-locals.el create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 links.org create mode 100644 server/.gitignore create mode 100644 server/CMakeLists.txt create mode 100644 server/CMakePresets.json create mode 100644 server/README create mode 100644 server/ct.sh create mode 100644 server/formal/.envrc create mode 100644 server/formal/.gitignore create mode 100644 server/formal/Makefile create mode 100644 server/formal/_CoqProject create mode 100644 server/formal/flake.lock create mode 100644 server/formal/flake.nix create mode 100644 server/formal/period.v create mode 100644 server/formal/period_seq.v create mode 100644 server/formal/util.v create mode 100755 server/hack/rwgps-get-auth-token.sh create mode 100644 server/locale/.gitignore create mode 100644 server/locale/README create mode 100755 server/locale/dev.sh create mode 100644 server/locale/en_US.po create mode 100644 server/locale/nl.po create mode 100755 server/locale/xget.sh create mode 100644 server/migrations/1_init_down.sql create mode 100644 server/migrations/1_init_up.sql create mode 100644 server/migrations/2_kaas_up.sql create mode 100644 server/routemon.pot create mode 100644 server/src/api.cpp create mode 100644 server/src/api.cppm create mode 100644 server/src/config.cpp create mode 100644 server/src/config.cppm create mode 100644 server/src/database.cppm create mode 100644 server/src/datex2.cppm create mode 100644 server/src/geo.cppm create mode 100644 server/src/gpx.cpp create mode 100644 server/src/gpx.cppm create mode 100644 server/src/http_client.cppm create mode 100644 server/src/http_common.cppm create mode 100644 server/src/http_server.cppm create mode 100644 server/src/locale.cppm create mode 100644 server/src/log.cppm create mode 100644 server/src/main.cpp create mode 100644 server/src/problem.cppm create mode 100644 server/src/req_ctx.cppm create mode 100644 server/src/routemon.cppm create mode 100644 server/src/rwgps.cppm create mode 100644 server/src/sqlite3.cppm create mode 100644 server/src/srv.cppm create mode 100644 server/src/time.cppm create mode 100644 server/src/trace.cppm create mode 100644 server/src/util.cppm create mode 100644 server/src/xml.cpp create mode 100644 server/src/xml.cppm create mode 100644 web/index.html create mode 100644 web/script.js create mode 100644 web/style.css diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..ca230d6 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,5 @@ +((c++-mode . ((indent-tabs-mode . nil))) + (css-mode . ((indent-tabs-mode . nil))) + (js-mode . ((indent-tabs-mode . nil))) + (js-json-mode . ((indent-tabs-mode . nil))) + (sh-mode . ((indent-tabs-mode . nil)))) diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..d031487 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +export CC=/usr/bin/clang +export CXX=/usr/bin/clang++ +export CMAKE_BUILD_PARALLEL_LEVEL=17 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8427b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +\#*# +*~ +.#* +# Nix +result +result-* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0e6aceb --- /dev/null +++ b/flake.lock @@ -0,0 +1,59 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1787736819, + "narHash": "sha256-IkjmqLoWzeqBAi1VIkdhDLMGjQDJ4suEDp59Zwxpswg=", + "rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1062397.9fbb54b33e91/nixexprs.tar.xz" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6756eed --- /dev/null +++ b/flake.nix @@ -0,0 +1,57 @@ +{ + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, ... }@inputs: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { inherit system; }; + boost = pkgs.boost191; + llvmVersion = "22"; + llvmPackages = pkgs."llvmPackages_${llvmVersion}"; + libstdcxxGcc = pkgs.gcc16.cc; + clang = llvmPackages.clang.override { + gccForLibs = libstdcxxGcc; + }; + inherit (clang) stdenv; + + routemon = stdenv.mkDerivation { + name = "routemon"; + src = ./server; + + buildInputs = with pkgs; [ boost pugixml expat icu openssl sqlite ]; + nativeBuildInputs = with pkgs; [ pkgs."llvmPackages_${llvmVersion}".clang-tools pkgs."clang_${llvmVersion}" boost cmake ninja gettext ]; + + hardeningDisable = [ + # Disable here because this breaks C++ modules (I suspect because it sets -D_FORTIFY_SOURCE=3) + # We set plenty of hardening options in CMakeLists.txt anyway. + "all" + ]; + + env.NIX_CFLAGS_COMPILE = toString [ + "-DLOCALEDIR=${builtins.placeholder "out"}/share/locale" + "-Wno-reserved-module-identifier" + "-isystem ${libstdcxxGcc}/include/c++/${libstdcxxGcc.version}/backward" + ]; + + cmakeFlags = [ + "--preset=nix-derivation" + "-DCMAKE_CXX_STDLIB_MODULES_JSON=${libstdcxxGcc}/lib/libstdc++.modules.json" + ]; + }; + in + { + packages.routemon = routemon; + + devShells.default = pkgs.mkShell.override { inherit stdenv; } { + buildInputs = [ pkgs.nix-index pkgs.nix-tree ]; + inputsFrom = [ routemon ]; + }; + + formatter = pkgs.nixpkgs-fmt; + }); +} + diff --git a/links.org b/links.org new file mode 100644 index 0000000..c9194e9 --- /dev/null +++ b/links.org @@ -0,0 +1,4 @@ +- https://github.com/DATEX-II-EU/Profiles +- https://opendata.ndw.nu/ + - =planningsfeed_wegwerkzaamheden_en_evenementen.xml.gz= +- https://www.mobilitaetsdaten.nrw/dataset/arbeitsstellen-nrw diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..0751de2 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,7 @@ +assets/ +src/*.o +src/*.d +build/ +config.json +*.sqlite3 +vendor/ diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..df4eb90 --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,86 @@ +cmake_minimum_required(VERSION 4.3) + +project(routemon LANGUAGES CXX) + +# Hardening options from https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html +add_compile_options( + # -O2 + # -g + -fno-omit-frame-pointer + -Wall -Wextra -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough + -Werror=format-security + -U_FORTIFY_SOURCE # -D_FORTIFY_SOURCE=3 (unfortunately breaks the std module) + -D_GLIBCXX_ASSERTIONS + -fstrict-flex-arrays=3 + -fstack-clash-protection -fstack-protector-strong + -fPIE -fcf-protection=full + -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -ftrivial-auto-var-init=zero + -DBOOST_ASIO_NO_DEPRECATED +) +add_link_options( + -pie + -Wl,-z,nodlopen -Wl,-z,noexecstack + -Wl,-z,relro -Wl,-z,now + -Wl,--as-needed -Wl,--no-copy-dt-needed-entries +) + +add_library(routemon_lib) +target_sources(routemon_lib + PUBLIC FILE_SET CXX_MODULES FILES + src/api.cppm + src/api.cpp + src/database.cppm + src/config.cppm + src/config.cpp + src/datex2.cppm + src/geo.cppm + src/gpx.cppm + src/gpx.cpp + src/http_client.cppm + src/http_common.cppm + src/http_server.cppm + src/locale.cppm + src/log.cppm + src/problem.cppm + src/req_ctx.cppm + src/routemon.cppm + src/rwgps.cppm + src/sqlite3.cppm + src/srv.cppm + src/time.cppm + src/trace.cppm + src/util.cppm + src/xml.cppm + src/xml.cpp +) + +add_executable(routemon src/main.cpp) +target_link_libraries(routemon routemon_lib) +install(TARGETS routemon) + +find_package(Boost 1.90 REQUIRED COMPONENTS json locale url) +target_link_libraries(routemon_lib Boost::headers Boost::json Boost::locale Boost::url) + +# Already arranged via Boost::locale but this makes it more explicit, I guess +find_package(ICU REQUIRED COMPONENTS data i18n uc) +target_link_libraries(routemon_lib ICU::data ICU::i18n ICU::uc) + +find_library(pugixml pugixml REQUIRED) +target_link_libraries(routemon_lib pugixml) + +find_package(OpenSSL REQUIRED) +target_link_libraries(routemon_lib OpenSSL::SSL) + +find_package(SQLite3 REQUIRED) +target_link_libraries(routemon_lib SQLite3::SQLite3) + +find_package(Gettext REQUIRED) +gettext_create_translations( + routemon.pot + ALL + locale/nl.po + locale/en_US.po +) + +find_package(expat REQUIRED) +target_link_libraries(routemon_lib expat::expat) diff --git a/server/CMakePresets.json b/server/CMakePresets.json new file mode 100644 index 0000000..ae7fd67 --- /dev/null +++ b/server/CMakePresets.json @@ -0,0 +1,29 @@ +{ + "version": 4, + "configurePresets": [ + { + "name": "arch", + "binaryDir": "${sourceDir}/build", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_CXX_STANDARD": "26", + "CMAKE_COLOR_DIAGNOSTICS": true, + "CMAKE_EXPERIMENTAL_CXX_IMPORT_STD": "f35a9ac6-8463-4d38-8eec-5d6008153e7d", + "CMAKE_CXX_EXTENSIONS": false, + "CMAKE_CXX_MODULE_STD": true, + "CMAKE_EXPORT_COMPILE_COMMANDS": true + } + }, + { + "name": "nix-derivation", + "cacheVariables": { + "CMAKE_CXX_STANDARD": "26", + "CMAKE_COLOR_DIAGNOSTICS": true, + "CMAKE_EXPERIMENTAL_CXX_IMPORT_STD": "451f2fe2-a8a2-47c3-bc32-94786d8fc91b", + "CMAKE_CXX_EXTENSIONS": false, + "CMAKE_CXX_MODULE_STD": true, + "CMAKE_EXPORT_COMPILE_COMMANDS": true + } + } + ] +} diff --git a/server/README b/server/README new file mode 100644 index 0000000..04d5d5a --- /dev/null +++ b/server/README @@ -0,0 +1,16 @@ +# Configure + +$ cmake --preset arch # (--fresh if the build folder already exists) + +# Build + +$ cmake --build build + +# Dependencies (and versions known to work) + +- CMake 4.3.4 +- clang version 22.1.6 +- Boost 1.91 +- pugixml 1.16 +- OpenSSL +- ICU \ No newline at end of file diff --git a/server/ct.sh b/server/ct.sh new file mode 100644 index 0000000..29ad28b --- /dev/null +++ b/server/ct.sh @@ -0,0 +1 @@ +clang-tidy -checks='boost-*,bugprone-*,clang-analyzer-*,concurrency-*,cppcoreguidlines-*,misc-*,-misc-include-cleaner,-misc-no-recursion,-misc-use-internal-linkage,modernize-*,performance-*,portability-*,radability-*' -p build '--exclude-header-filter=.*' src/time.cpp diff --git a/server/formal/.envrc b/server/formal/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/server/formal/.envrc @@ -0,0 +1 @@ +use flake diff --git a/server/formal/.gitignore b/server/formal/.gitignore new file mode 100644 index 0000000..9023475 --- /dev/null +++ b/server/formal/.gitignore @@ -0,0 +1,17 @@ +*.aux +*.glob +*.vio +*.vo +*.vok +*.vos +.CoqMakefile.d +.Makefile.coq.d +.direnv +.lia.cache +Makefile.coq +Makefile.coq.conf +*#*.v# +*#*.vok# +*~ +.#* +\#*# \ No newline at end of file diff --git a/server/formal/Makefile b/server/formal/Makefile new file mode 100644 index 0000000..ac8dba0 --- /dev/null +++ b/server/formal/Makefile @@ -0,0 +1,55 @@ +# Default target +all: Makefile.coq + +@$(MAKE) -f Makefile.coq all +.PHONY: all + +# Permit local customization +-include Makefile.local + +# Forward most targets to Coq makefile (with some trick to make this phony) +%: Makefile.coq phony + @#echo "Forwarding $@" + +@$(MAKE) -f Makefile.coq $@ +phony: ; +.PHONY: phony + +clean: Makefile.coq + +@$(MAKE) -f Makefile.coq clean + @# Make sure not to enter the `_opam` folder. + find [a-z]*/ \( -name "*.d" -o -name "*.vo" -o -name "*.vo[sk]" -o -name "*.aux" -o -name "*.cache" -o -name "*.glob" -o -name "*.vio" \) -print -delete || true + rm -f Makefile.coq .lia.cache builddep/* +.PHONY: clean + +# Create Coq Makefile. +Makefile.coq: _CoqProject Makefile + "$(COQBIN)coq_makefile" -f _CoqProject -o Makefile.coq $(EXTRA_COQFILES) + +# Install build-dependencies +OPAMFILES=$(wildcard *.opam) +BUILDDEPFILES=$(addsuffix -builddep.opam, $(addprefix builddep/,$(basename $(OPAMFILES)))) + +builddep/%-builddep.opam: %.opam Makefile + @echo "# Creating builddep package for $<." + @mkdir -p builddep + @sed <$< -E 's/^(build|install|remove):.*/\1: []/; s/"(.*)"(.*= *version.*)$$/"\1-builddep"\2/;' >$@ + +builddep-opamfiles: $(BUILDDEPFILES) +.PHONY: builddep-opamfiles + +builddep: builddep-opamfiles + @# We want opam to not just install the build-deps now, but to also keep satisfying these + @# constraints. Otherwise, `opam upgrade` may well update some packages to versions + @# that are incompatible with our build requirements. + @# To achieve this, we create a fake opam package that has our build-dependencies as + @# dependencies, but does not actually install anything itself. + @echo "# Installing builddep packages." + @opam install $(OPAMFLAGS) $(BUILDDEPFILES) +.PHONY: builddep + +# Backwards compatibility target +build-dep: builddep +.PHONY: build-dep + +# Some files that do *not* need to be forwarded to Makefile.coq. +# ("::" lets Makefile.local overwrite this.) +Makefile Makefile.local _CoqProject $(OPAMFILES):: ; diff --git a/server/formal/_CoqProject b/server/formal/_CoqProject new file mode 100644 index 0000000..92d635c --- /dev/null +++ b/server/formal/_CoqProject @@ -0,0 +1,5 @@ +-Q . routemon + +util.v +period.v +period_seq.v \ No newline at end of file diff --git a/server/formal/flake.lock b/server/formal/flake.lock new file mode 100644 index 0000000..f4a7de9 --- /dev/null +++ b/server/formal/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777077449, + "narHash": "sha256-AIiMJiqvGrN4HyLEbKAoCSRRYn0rnlW5VbKNIMIYqm4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a4bf06618f0b5ee50f14ed8f0da77d34ecc19160", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/server/formal/flake.nix b/server/formal/flake.nix new file mode 100644 index 0000000..57efa11 --- /dev/null +++ b/server/formal/flake.nix @@ -0,0 +1,26 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + # From 22-04-2026 + stdpp = with pkgs; coqPackages.lib.overrideCoqDerivation { + version = "dev"; + release."dev".sha256 = "hN+sEZcIaFoFF2+4dStTc0TRz5A03US6csEk5q0r/z8="; + release."dev".rev = "d3c67aa46ed22b1e593457cd34fc711f1a53b8be"; + } coqPackages.stdpp; + in + { + devShells.default = with pkgs; mkShell { + buildInputs = [ coq stdpp ]; + }; + + formatter = pkgs.nixpkgs-fmt; + }); +} diff --git a/server/formal/period.v b/server/formal/period.v new file mode 100644 index 0000000..82921db --- /dev/null +++ b/server/formal/period.v @@ -0,0 +1,828 @@ +From stdpp Require Import numbers option sorting ssreflect. +From stdpp Require Import options. +From routemon Require Import util. + +Definition timestamp := Z. +Variant limit := + | NegInftyLimit + | TsLimit (x : timestamp) + | PosInftyLimit. +Instance limit_eq_dec : EqDecision limit. +Proof. solve_decision. Qed. + +Notation "-∞" := NegInftyLimit. +Notation "+∞" := PosInftyLimit. +Coercion TsLimit : timestamp >-> limit. + +(* The interval [start, end). Considered empty when start >= end. *) +Record period := + Period + { period_start : limit + ; period_end : limit + }. +Notation "'[' s ',' e ')'" := (Period s e). + +(* Consider making an inductive variant of these? *) +Definition limit_le (l1 l2 : limit) := + match l1, l2 with + | -∞, _ | _, +∞ => True + | TsLimit t1, TsLimit t2 => (t1 ≤ t2)%Z + | _, _ => False + end. +Arguments limit_le !_ !_ / : assert. +Definition limit_lt l1 l2 := + match l1 with + | -∞ => + match l2 with + | -∞ => False + | _ => True + end + | TsLimit t1 => + match l2 with + | -∞ => False + | TsLimit t2 => (t1 < t2)%Z + | +∞ => True + end + | +∞ => False + end. +Arguments limit_lt !_ !_ / : assert. +Instance limit_le_dec : RelDecision limit_le. +Proof. intros [] []; simpl; solve_decision. Qed. +Instance limit_lt_dec : RelDecision limit_lt. +Proof. intros [] []; simpl; solve_decision. Qed. +Instance limit_lt_pi l1 l2 : ProofIrrel (limit_lt l1 l2). +Proof. destruct l1, l2; apply _. Qed. + +Instance relation_equiv {A} : Equiv (relation A) := + λ R1 R2, ∀ x y, R1 x y ↔ R2 x y. + +Lemma strict_limit_le_limit_lt : + strict limit_le ≡ limit_lt. +Proof. + split. + - intros []. destruct x, y; simpl in *; try done. lia. + - intros H. destruct x, y; unfold strict; simpl in *; try done; auto with lia. +Qed. + +Instance : Reflexive limit_le. +Proof. intros l. by destruct l; simpl. Qed. +Instance : Transitive limit_le. +Proof. intros [] [] []; simpl; try done. lia. Qed. +Instance : PreOrder limit_le. +Proof. constructor; apply _. Qed. +Instance : AntiSymm (=) limit_le. +Proof. + intros [] []; simpl; try done. + intros H1 H2. f_equal. by apply Z.le_antisymm. +Qed. +Instance : PartialOrder limit_le. +Proof. constructor; apply _. Qed. +Instance : Trichotomy (strict limit_le). +Proof with auto with lia. + intros [] []; unfold strict; simpl... + destruct (Z.lt_trichotomy x x0) as [H|[->|H]]... +Qed. +Instance : TotalOrder limit_le. +Proof. constructor; apply _. Qed. + +Instance : StrictOrder (strict limit_le) := _. +(* TODO: apparently useless?? +Instance rel_equiv_proper {A} (x y : A) : Proper ((≡) ==> (↔)) (λ R, R x y). +Proof. easy. Qed. +Search Proper iff eq. +*) +Instance complement_equiv {A} : Proper ((≡) ==> (≡)) (@complement A). +Proof. intros R1 R2 HR12. split; unfold complement; intros Hequiv []%HR12%Hequiv. Qed. +Instance Reflexive_equiv {A} : Proper ((≡) ==> (↔)) (@Reflexive A). +Proof. + intros R1 R2 Hequiv. unfold Reflexive. + split; intros H x; by apply Hequiv. +Qed. +Instance Irreflexive_equiv {A} : Proper ((≡) ==> (↔)) (@Irreflexive A). +Proof. unfold Irreflexive. by intros R1 R2 ->. Qed. +Instance Transitive_equiv {A} : Proper ((≡) ==> (↔)) (@Transitive A). +Proof. + intros R1 R2 Hequiv. unfold Transitive. + by split; intros H x y z Hxy%Hequiv Hyz%Hequiv; eapply Hequiv, H. +Qed. +Instance StrictOrder_equiv {A} : Proper ((≡) ==> (↔)) (@StrictOrder A). +Proof. + intros R1 R2 Hequiv. split; intros [Hirr Htrans]. + - by rewrite ->Hequiv in Hirr, Htrans. + - by rewrite <-Hequiv in Hirr, Htrans. +Qed. +Instance Trichotomy_equiv {A} : Proper ((≡) ==> (↔)) (@Trichotomy A). +Proof. + intros R1 R2 Hequiv. split; intros. + - intros x y. by rewrite -(Hequiv x y) -(Hequiv y x). + - intros x y. by rewrite (Hequiv x y) (Hequiv y x). +Qed. + +Instance : StrictOrder limit_lt. +Proof. rewrite -strict_limit_le_limit_lt. apply _. Qed. +Instance : Trichotomy limit_lt. +Proof. rewrite -strict_limit_le_limit_lt. apply _. Qed. + +Definition limit_lt_ts' (l : limit) (t2 : timestamp) := + match l with + | -∞ => True + | TsLimit t1 => (t1 < t2)%Z + | +∞ => False + end. +Definition ts_le_limit' (t1 : timestamp) (l : limit) := + match l with + | -∞ => False + | TsLimit t2 => (t1 ≤ t2)%Z + | +∞ => True + end. +Lemma limit_lt_limit_lt_ts' l t2 : limit_lt_ts' l t2 ↔ limit_lt l t2. +Proof. by destruct l. Qed. +Lemma ts_le_limit'_limit_le t1 l : ts_le_limit' t1 l ↔ limit_le t1 l. +Proof. by destruct l. Qed. + +Declare Scope limit_scope. +Delimit Scope limit_scope with lim. +Notation "l1 < l2" := (limit_lt l1 l2) : limit_scope. +Notation "l1 ≤ l2" := (limit_le l1 l2) : limit_scope. +Notation "l1 < l2 < l3" := (l1 < l2 ∧ l2 < l3)%lim : limit_scope. +Notation "l1 ≤ l2 < l3" := (l1 ≤ l2 ∧ l2 < l3)%lim : limit_scope. +Notation "l1 < l2 ≤ l3" := (l1 < l2 ∧ l2 ≤ l3)%lim : limit_scope. +Notation "l1 ≤ l2 ≤ l3" := (l1 ≤ l2 ∧ l2 ≤ l3)%lim : limit_scope. +Open Scope limit_scope. + +Instance period_elem_of : ElemOf timestamp period := + λ t '[s, e), (s ≤ t < e). +Instance period_elem_of_dec t (p : period) : Decision (t ∈ p). +Proof. destruct p as [s e]. apply _. Qed. + +Lemma limit_le_lt l1 l2 : l1 < l2 ↔ l1 ≤ l2 ∧ l1 ≠ l2. +Proof. by rewrite -(strict_limit_le_limit_lt l1 l2) strict_spec_alt. Qed. + +Lemma limit_le_cases {l1 l2} : l1 ≤ l2 ↔ l1 = l2 ∨ l1 < l2. +Proof. + rewrite -(strict_limit_le_limit_lt l1 l2) strict_spec_alt. split. + - intros Hl12. destruct (decide (l1 = l2)) as [<-|Hne]; tauto. + - by intros [<-|[Hl12 _]]. +Qed. + +Lemma limit_lt_le_lt {l1} l2 {l3} : l1 ≤ l2 < l3 → l1 < l3. +Proof. by intros [[<-|?]%limit_le_cases ?]; last etrans. Qed. + +Definition period_empty '[s, e) := e ≤ s. +Definition period_empty_alt (p : period) := ∀ t, t ∉ p. +Lemma period_empty_alt_iff p : period_empty p ↔ period_empty_alt p. +Proof. + destruct p as [s e]. + rewrite /period_empty /period_empty_alt /=. + split; intros H. + - intros t [contra []]%limit_lt_le_lt%limit_le_lt. + by eapply (anti_symm limit_le). + - destruct s as [|s|], e as [|e|]; try done. + + exfalso. apply (H (Z.pred e)). rewrite /elem_of /period_elem_of /=. lia. + + exfalso. by apply (H 0%Z). + + rewrite /elem_of /period_elem_of /= in H. + specialize (H s). simpl. lia. + + exfalso. apply (H s). rewrite /elem_of /period_elem_of /=. lia. +Qed. +Instance period_empty_dec p : Decision (period_empty p). +Proof. destruct p as [s e]. solve_decision. Qed. + +Definition period_nonempty '[s, e) := s < e. +Instance period_nonempty_dec p : Decision (period_nonempty p). +Proof. destruct p. apply _. Qed. +Instance period_nonempty_pi p : ProofIrrel (period_nonempty p). +Proof. destruct p. apply _. Qed. + +Instance period_equiv : Equiv period := + λ p1 p2, ∀ t, t ∈ p1 ↔ t ∈ p2. +Instance period_equiv_reflexive : Reflexive period_equiv. +Proof. done. Qed. +Instance period_equiv_trans : Transitive period_equiv. +Proof. intros p1 p2 p3 H1 H2 t. by rewrite H1. Qed. +Instance period_equiv_symm : Symmetric period_equiv. +Proof. by intros p1 p2 H t. Qed. +Instance period_equiv_equiv : Equivalence period_equiv. +Proof. constructor; apply _. Qed. + +(* All empty periods are equivalent *) +Lemma period_empty_equiv p1 p2 : period_empty p1 → period_empty p2 ↔ p1 ≡ p2. +Proof. + intros Hp1%period_empty_alt_iff. split. + - intros Hp2%period_empty_alt_iff. intros t. + split; [intros []%(Hp1 _) | intros []%(Hp2 _)]. + - intros Hequiv. apply period_empty_alt_iff. + intros t []%Hequiv%(Hp1 _). +Qed. + +Instance empty_period : Empty period := [TsLimit 0%Z, TsLimit 0%Z). +Definition empty_period_empty : period_empty empty_period. +Proof. done. Qed. + +Definition limit_min (l1 l2 : limit) := if decide (l1 ≤ l2) then l1 else l2. +Definition limit_max (l1 l2 : limit) := if decide (l1 ≤ l2) then l2 else l1. + +Notation "l1 '`min`' l2" := (limit_min l1 l2) : limit_scope. +Notation "l1 '`max`' l2" := (limit_max l1 l2) : limit_scope. + +Definition limit_min_ts (t1 t2 : timestamp) : + t1 `min` t2 = TsLimit (t1 `min` t2)%Z. +Proof. + unfold limit_min. + destruct (decide (t1 ≤ t2)); + simpl in *; f_equal; lia. +Qed. + +Definition limit_max_ts (t1 t2 : timestamp) : + t1 `max` t2 = TsLimit (t1 `max` t2)%Z. +Proof. + unfold limit_max. + destruct (decide (t1 ≤ t2)); + simpl in *; f_equal; lia. +Qed. + +Instance period_intersection : Intersection period := λ '[s1, e1) '[s2, e2), + [ s1 `max` s2, e1 `min` e2 ). + +Lemma intersect_and (p1 p2 : period) t : + t ∈ (p1 ∩ p2) ↔ t ∈ p1 ∧ t ∈ p2. +Proof. + (* It really should be possible to optimize this proof somehow. *) + destruct p1 as [[|s1|] [|e1|]], p2 as [[|s2|] [|e2|]]; + rewrite /intersection /period_intersection /elem_of /period_elem_of /limit_min /limit_max /limit_le /limit_lt /=; + repeat case_decide; tauto || lia. +Qed. + +(* The points in time given by p1 except those given by p2, given as a before/after pair. *) +Definition except '[s1, e1) '[s2, e2) : period * period := + ( [ s1, e1 `min` s2 ), + [ s1 `max` e2, e1 ) ). + +Lemma limit_lt_ne l1 l2 : l1 < l2 → l1 ≠ l2. +Proof. rewrite -(strict_limit_le_limit_lt l1 l2) strict_spec_alt. easy. Qed. + +Lemma not_limit_le l1 l2 : ¬ (l1 ≤ l2) ↔ l2 < l1. +Proof. + destruct (trichotomy limit_lt l1 l2) as [Hl12|[<-|Hl21]]. + - split; intros H. + + exfalso. apply H, limit_le_cases. by right. + + exfalso. by eapply asymmetry. + - split; intros H. + + exfalso. apply H, limit_le_cases. by left. + + by apply (_ : Irreflexive limit_lt) in H. + - split; intros H; first done. + intros [<-|Hl12]%limit_le_cases. + + by apply (_ : Irreflexive limit_lt) in H. + + by eapply asymmetry. +Qed. + +Lemma not_limit_le' : complement limit_le ≡ flip limit_lt. +Proof. apply: not_limit_le. Qed. + +Instance relation_equiv_reflexive {A} : Reflexive (@relation_equiv A). +Proof. done. Qed. +Instance relation_equiv_trans {A} : Transitive (@relation_equiv A). +Proof. intros R1 R2 R3 H12 H23 x y. by rewrite H12 -H23. Qed. +Instance relation_equiv_symm {A} : Symmetric (@relation_equiv A). +Proof. by intros R1 R2 H12 x y. Qed. +Instance relation_equiv_equiv {A} : Equivalence (@relation_equiv A). +Proof. constructor; apply _. Qed. + +Lemma relation_flip_equiv {A} : Proper ((≡@{relation A}) ==> (≡)) flip. +Proof. intros R1 R2 H12 x y. simpl. by rewrite (H12 y x). Qed. + +(* Could also be more generic *) +Lemma relation_flip_involutive {A} (R : relation A) : flip (flip R) ≡ R. +Proof. done. Qed. + +Lemma complement_involutive {A} `{!RelDecision (R : relation A)} : complement (complement R) ≡ R. +Proof. + intros x y. split; intros Hxy. + - by destruct (decide (R x y)). + - by apply. +Qed. + +Lemma not_limit_lt' : complement limit_lt ≡ flip limit_le. +Proof. + rewrite -(relation_flip_involutive limit_lt) complement_inverse. + trans (flip (complement (complement limit_le))). + { apply relation_flip_equiv, complement_equiv, symmetry, not_limit_le'. } + apply relation_flip_equiv, complement_involutive. +Qed. + +Lemma not_limit_lt l1 l2 : ¬ (l1 < l2) ↔ l2 ≤ l1. +Proof. apply not_limit_lt'. Qed. + +(* TODO: make conclusion positive? *) +Lemma period_nonempty_equiv_L_1 (s1 e1 s2 e2 : limit) : + period_nonempty [s1, e1) → + period_nonempty [s2, e2) → + [s1, e1) ≡ [s2, e2) → + ¬ s1 < s2. +Proof. + unfold period_nonempty. + intros Hne1 Hne2 Hequiv Hs12. + destruct s2 as [|s2|]; [by destruct s1|..|by destruct s1]. + destruct s1 as [|s1|]; last done. + * assert (Hs2a : s2 ∈ [s2, e2)). + { unfold elem_of, period_elem_of. by destruct e2. } + pose proof (proj2 (Hequiv s2) Hs2a) as [_ Hs2b]. + assert (Hs2c : Z.pred s2 ∈ [-∞, e1)). + { unfold elem_of, period_elem_of. + by destruct e1 as [|e1|]; [|simpl in *; lia|]. } + pose proof (proj1 (Hequiv (Z.pred s2)) Hs2c) as [contra _]. + simpl in contra. lia. + * assert (Hs1 : s1 ∈ [s1, e1)). + { unfold elem_of, period_elem_of. by destruct e1. } + pose proof (proj1 (Hequiv s1) Hs1) as [[Heq|Heq]%limit_le_cases _]. + { rewrite Heq in Hs12. by eapply (_ : Irreflexive limit_lt). } + by eapply asymmetry. +Qed. + +(* TODO: make conclusion positive? *) +Lemma period_nonempty_equiv_L_2 (s1 e1 s2 e2 : limit) : + period_nonempty [s1, e1) → + period_nonempty [s2, e2) → + [s1, e1) ≡ [s2, e2) → + ¬ e1 < e2. +Proof. + intros Hne1 Hne2 Hequiv He12. + destruct e1 as [|e1|]; [by destruct s1|..|done]. + destruct e2 as [|e2|]; first done. + * (* e2 - 1 ∈ [s2, e2) → e2 - 1 ∈ [s1, e1) → s1 ≤ e2 - 1 < e1 → e2 ≤ e1 → e2 = e1 ∨ e2 < e1 *) + assert (He2a : Z.pred e2 ∈ [s2, e2)). + { unfold elem_of, period_elem_of. + by destruct s2 as [|s2|]; [simpl in *; lia..|]. } + pose proof (proj2 (Hequiv (Z.pred e2)) He2a) as [_ He2b]. + simpl in *. lia. + * (* We want to plug e1 into the right side to get a + contradiction, so we need s2 ≤ e1. It suffices to show that + s2 ≤ e1 - 1 *) + assert (He1a : Z.pred e1 ∈ [s1, e1)). + { unfold elem_of, period_elem_of. + by destruct s1 as [|s1|]; [simpl in *; lia..|]. } + pose proof (proj1 (Hequiv (Z.pred e1)) He1a) as [He1b _]. + assert (He1c : e1 ∈ [s2, +∞)). + { unfold elem_of, period_elem_of. + by destruct s2 as [|s2|]; [|simpl in *; lia|]. } + pose proof (proj2 (Hequiv e1) He1c) as [_ []%(_ : Irreflexive limit_lt)]. +Qed. + +Lemma period_nonempty_equiv_L p1 p2 : + period_nonempty p1 → + period_nonempty p2 → + p1 ≡ p2 → p1 = p2. +Proof. + destruct p1 as [s1 e1], p2 as [s2 e2]. + unfold equiv, period_equiv. + intros Hne1 Hne2 Hequiv. + f_equal. + - destruct (decide (s1 < s2)) as [Hs12|[<-|Hs21]%not_limit_lt%limit_le_cases]; [|done|]. + + exfalso. by apply (period_nonempty_equiv_L_1 s1 e1 s2 e2). + + exfalso. by apply (period_nonempty_equiv_L_1 s2 e2 s1 e1). + - destruct (decide (e1 < e2)) as [He12|[<-|He21]%not_limit_lt%limit_le_cases]; [|done|]. + + exfalso. by apply (period_nonempty_equiv_L_2 s1 e1 s2 e2). + + exfalso. by apply (period_nonempty_equiv_L_2 s2 e2 s1 e1). +Qed. + +Definition period_empty_not_nonempty p : ¬ period_empty p ↔ period_nonempty p. +Proof. destruct p. apply not_limit_le. Qed. + +Lemma limit_lt_min l1 l2 l3 : + l1 < l2 ∧ l1 < l3 ↔ l1 < l2 `min` l3. +Proof. + split. + - intros [Hl12 Hl13]. unfold limit_min. by case_decide. + - unfold limit_min. intros H. case_decide. + + split; first done. + apply limit_le_cases in H0 as [<-|H0]; first done. + by etrans. + + apply not_limit_le in H0. + by split; first etrans. +Qed. + +Lemma limit_max_le l1 l2 l3 : + l1 ≤ l3 ∧ l2 ≤ l3 ↔ l1 `max` l2 ≤ l3. +Proof. + split. + - intros [Hl12 Hl23]. unfold limit_max. by case_decide. + - unfold limit_max. intros H. case_decide. + + by split; first etrans. + + apply not_limit_le in H0. split; first done. + apply limit_le_lt in H0 as [H0 _]. by etrans. +Qed. +Lemma limit_le_max l1 l2 l3 : + l1 ≤ l2 `max` l3 ↔ l1 ≤ l2 ∨ l1 ≤ l3. +Proof. + unfold limit_max. + destruct (decide (l2 ≤ l3)) as [Hl23|Hl23%not_limit_le]. + - split; first tauto. intros [H|H]; last done. by etrans. + - split; first tauto. intros [H|H]; first done. + by trans l3; last (apply limit_le_cases; right). +Qed. + +Lemma except_lem p1 p2 t : + t ∈ p1 ∧ t ∉ p2 ↔ + t ∈ (except p1 p2).1 ∨ t ∈ (except p1 p2).2. +Proof. + destruct p1 as [s1 e1], p2 as [s2 e2]. split. + - intros [Hp1 Hp2]. + (* on the left if t < s2, on the right if e2 ≤ t *) + destruct (decide (t < s2)) as [Hts2|Hts2]. + + (* t < s2 *) + left. simpl. split. + * apply Hp1. + * apply limit_lt_min. split; last done. + rewrite /elem_of /period_elem_of in Hp1. easy. + + (* ¬ (t < s2) (↔ s2 ≤ t) *) + apply not_limit_lt in Hts2. + right. simpl. split. + * apply limit_max_le. split. + -- apply Hp1. + -- apply not_limit_lt. intros contra. by apply Hp2. + * apply Hp1. + - intros [H|H]; simpl in *. + + split. + * unfold elem_of, period_elem_of in *. split. + -- apply H. + -- by destruct H as [_ [H _]%limit_lt_min]. + * unfold elem_of, period_elem_of in *. + destruct H as [H1 [H2 H3]%limit_lt_min]. + intros [Hc1 Hc2]. + apply limit_le_cases in Hc1 as [Hc1|Hc1]. + -- inv Hc1. by apply (_ : Irreflexive limit_lt) in H3. + -- eapply asymmetry; [apply H3 | apply Hc1]. + + unfold elem_of, period_elem_of in H. + rewrite -limit_max_le in H. destruct H as [[H1 H2] H3]. + split; first done. + intros [Hc1 Hc2]. + apply limit_le_cases in H2 as [H2|H2]. + -- inv H2. by apply (_ : Irreflexive limit_lt) in Hc2. + -- eapply asymmetry; [apply H2 | apply Hc2]. +Qed. + +Lemma limit_max_lt l1 l2 l3 : + l1 `max` l2 < l3 ↔ l1 < l3 ∧ l2 < l3. +Proof. + unfold limit_max. + destruct (decide (l1 ≤ l2)) as [Hl12|Hl12%not_limit_le]. + - split; last easy. intros H. by split; first eapply limit_lt_le_lt. + - split; last easy. intros H. by split; last etrans. +Qed. + +Lemma limit_min_lt l1 l2 l3 : + l1 `min` l2 < l3 ↔ l1 < l3 ∨ l2 < l3. +Proof. + unfold limit_min. + destruct (decide (l1 ≤ l2)%lim) as [Hl12|Hl12%not_limit_le]. + - split; first tauto. by intros [H|H]; last eapply limit_lt_le_lt. + - split; first tauto. by intros [H|H]; first etrans. +Qed. + +Lemma limit_lt_max l1 l2 l3 : + l1 < l2 `max` l3 ↔ l1 < l2 ∨ l1 < l3. +Proof. + unfold limit_max. + destruct (decide (l2 ≤ l3)) as [Hl23|Hl23%not_limit_le]. + - split; first tauto. intros [H|H]; last done. + by apply limit_le_cases in Hl23 as [<-|Hl23]; last etrans. + - split; first tauto. by intros [H|H]; last etrans. +Qed. + +Instance limit_min_comm : Comm (=) limit_min. +Proof. + unfold limit_min. + intros [] []; repeat case_decide; + try done; simpl in *; f_equal; lia. +Qed. +Instance limit_max_comm : Comm (=) limit_max. +Proof. + unfold limit_max. + intros [] []; repeat case_decide; + try done; simpl in *; f_equal; lia. +Qed. + +Lemma limit_max_eq_l l1 l2 : l1 `max` l2 = l1 ↔ l2 ≤ l1. +Proof. + unfold limit_max. case_decide; split. + - by intros ->. + - intros H12. by eapply (_ : AntiSymm (=) limit_le). + - intros _. apply limit_le_cases. right. + by apply not_limit_le. + - by intros _. +Qed. +Lemma limit_max_eq_r l1 l2 : l1 `max` l2 = l2 ↔ l1 ≤ l2. +Proof. rewrite [l1 `max` l2]comm. apply limit_max_eq_l. Qed. + +Lemma limit_max_l l1 l2 : l2 ≤ l1 → l1 `max` l2 = l1. +Proof. apply limit_max_eq_l. Qed. +Lemma limit_max_r l1 l2 : l1 ≤ l2 → l1 `max` l2 = l2. +Proof. apply limit_max_eq_r. Qed. + +Definition ne_period := { p : period | period_nonempty p }. + +Instance ne_period_elem_of : ElemOf timestamp ne_period := + λ t p, t ∈ `p. +Instance ne_period_elem_of_dec t (p : ne_period) : Decision (t ∈ p). +Proof. apply _. Qed. + +(* TODO: rename to ne_period_before *) +Definition period_before '([s1, e1) ↾ _ : ne_period) '([s2, e2) ↾ _ : ne_period) := + e1 < s2. + +Instance period_before_pi p1 p2 : ProofIrrel (period_before p1 p2). +Proof. destruct p1 as [[s1 e1] Hne1], p2 as [[s2 e2] Hne2]. apply _. Qed. + +Instance period_before_trans : Transitive period_before. +Proof. + intros [[s1 e1] Hne1] [[s2 e2] Hne2] [[s3 e3] Hne3] H1 H2. + unfold period_before in *. simpl in *. + by trans s2; last trans e2. +Qed. + +Instance period_before_irrefl : Irreflexive period_before. +Proof. + intros [[s e] Hne] Hp. simpl in *. + eapply (_ : Irreflexive limit_lt). by etrans. +Qed. + +Instance period_before_strict_order : StrictOrder period_before. +Proof. split; apply _. Qed. + +Definition ne_period_rel (R : relation ne_period) : relation period := + λ p1 p2, ∃ H1 H2, R (p1 ↾ H1) (p2 ↾ H2). + +Instance ne_period_rel_trans `{!Transitive R} : Transitive (ne_period_rel R). +Proof. + intros p1 p2 p3 (H1 & H2 & HR12) (H2' & H3 & HR23). + exists H1, H3. replace H2' with H2 in HR23; last apply proof_irrel. + by etrans. +Qed. + +Instance ne_period_rel_irrefl `{!Irreflexive R} : Irreflexive (ne_period_rel R). +Proof. + intros p. intros (H1 & H2 & HR). + replace H2 with H1 in HR; last apply proof_irrel. + by apply (_ : Irreflexive R) in HR. +Qed. + +Instance ne_period_rel_pi (R : relation ne_period) `{!∀ x y, ProofIrrel (R x y)} x y : + ProofIrrel (ne_period_rel R x y). +Proof. apply _. Qed. + +Lemma except_parts_order p1 p2 : + period_nonempty p2 → + period_nonempty (except p1 p2).1 → + period_nonempty (except p1 p2).2 → + ne_period_rel period_before (except p1 p2).1 (except p1 p2).2. +Proof. + destruct p1 as [s1 e1], p2 as [s2 e2]. simpl. intros H0 Hne1 Hne2. + unfold period_before. split; [done|split; [done|]]. + apply limit_min_lt. right. apply limit_lt_max. right. apply H0. +Qed. + +Definition period_nonempty_alt (p : period) := ∃ t, t ∈ p. + +Lemma period_nonempty_alt_iff p : + period_nonempty p ↔ period_nonempty_alt p. +Proof. + unfold period_nonempty, period_nonempty_alt. + destruct p as [s e]. split. + - intros Hlt. destruct s as [|s|]; last done. + + destruct e as [|e|]; first done. + * exists (Z.pred e). by split; last (simpl; lia). + * exists 0%Z. done. + + exists s. done. + - intros [t Ht]. by eapply limit_lt_le_lt. +Qed. + +Instance period_eq_dec : EqDecision period. +Proof. solve_decision. Qed. + +Instance period_disjoint : Disjoint period := + λ p1 p2, period_empty (p1 ∩ p2). + +Instance period_intersection_comm : Comm (=) period_intersection. +Proof. + intros [s1 e1] [s2 e2]. + by rewrite /= [s2 `max` s1]comm [e2 `min` e1]comm. +Qed. +Instance period_disjoint_symm : Symmetric period_disjoint. +Proof. intros p1 p2. unfold period_disjoint. by rewrite comm. Qed. + +Lemma limit_min_eq_l l1 l2 : l1 `min` l2 = l1 ↔ l1 ≤ l2. +Proof. + unfold limit_min. case_decide; first done. split. + - intros ->. exfalso. by apply H. + - intros []%H. +Qed. +Lemma limit_min_eq_r l1 l2 : l1 `min` l2 = l2 ↔ l2 ≤ l1. +Proof. rewrite [l1 `min` l2]comm. apply limit_min_eq_l. Qed. + +Lemma limit_min_l l1 l2 : l1 ≤ l2 → l1 `min` l2 = l1. +Proof. apply limit_min_eq_l. Qed. +Lemma limit_min_r l1 l2 : l2 ≤ l1 → l1 `min` l2 = l2. +Proof. apply limit_min_eq_r. Qed. + +Lemma limit_lt_le_trans {l1} l2 {l3} : l1 < l2 → l2 ≤ l3 → l1 < l3. +Proof. by intros Hlt12 [->|Hlt23]%limit_le_cases; last etrans. Qed. + +Instance period_union : Union period := + λ '[s1, e1) '[s2, e2), [s1 `min` s2, e1 `max` e2). + +Lemma limit_min_le l1 l2 l3 : + l1 ≤ l3 ∨ l2 ≤ l3 ↔ + l1 `min` l2 ≤ l3. +Proof. + unfold limit_min. case_decide; split. + - by intros [H13|H23]; last trans l2. + - intros H13. by left. + - apply not_limit_le in H. intros [H13|H23]; last done. + trans l1; last done. + apply limit_le_cases. by right. + - intros H23. by right. +Qed. + +Lemma limit_le_min l1 l2 l3 : + l1 ≤ l2 ∧ l1 ≤ l3 ↔ + l1 ≤ l2 `min` l3. +Proof. + unfold limit_min. case_decide; split. + - by intros [H12 _]. + - intros ?. by split; last trans l2. + - by intros [_ H13]. + - intros ?. split; last done. + apply not_limit_le in H. + trans l3; first done. + apply limit_le_cases. by right. +Qed. + +Lemma period_union_lem_1 t (p1 p2 : period) : + t ∈ p1 ∨ t ∈ p2 → t ∈ p1 ∪ p2. +Proof. + destruct p1 as [s1 e1], p2 as [s2 e2]. + unfold union, period_union. + intros [Ht|Ht]; split. + - apply limit_min_le. left. apply Ht. + - apply limit_lt_max. left. apply Ht. + - apply limit_min_le. right. apply Ht. + - apply limit_lt_max. right. apply Ht. +Qed. + +Definition unifiable '[s1, e1) '[s2, e2) := + s2 ≤ e1 ∧ s1 ≤ e2. + +Instance unifiable_dec : RelDecision unifiable. +Proof. intros [] []. solve_decision. Qed. + +Lemma not_limit_le_lt l1 l2 l3 : + ¬ l1 ≤ l2 < l3 ↔ l2 < l1 ∨ l3 ≤ l2. +Proof. + split. + - intros H123. + destruct (decide (l2 < l1)) as [?|H21%not_limit_lt]; first by left. + destruct (decide (l3 ≤ l2)) as [?|H32%not_limit_le]; first by right. + exfalso. by apply H123. + - intros [H21|H32] contra. + + eapply not_limit_le; [apply H21|apply contra]. + + eapply not_limit_lt; [apply H32|apply contra]. +Qed. + +Lemma limit_lt_le l1 l2 : l1 < l2 → l1 ≤ l2. +Proof. intros Hlt. apply limit_le_cases. by right. Qed. + +Lemma period_union_lem_2 t (p1 p2 : period) : + unifiable p1 p2 → + t ∈ p1 ∪ p2 → t ∈ p1 ∨ t ∈ p2. +Proof. + intros Hunif Hunion. + destruct (decide (t ∈ p1)) as [?|Ht1]; first by left. + destruct (decide (t ∈ p2)) as [?|Ht2]; first by right. + exfalso. + + destruct p1 as [s1 e1], p2 as [s2 e2]. + unfold elem_of, period_elem_of in *. + simpl in *. destruct Hunif as [Hunif1 Hunif2]. + + (* If t is not in p1, then it must be in p2 *) + apply Ht2. clear Ht2. + apply not_limit_le_lt in Ht1. + destruct Ht1 as [Ht1|Ht1]. + - (* t is not in p1 because it is before p1 (where p2 must hence be) *) + destruct Hunion as [Hunion1 Hunion2]. + apply limit_min_le in Hunion1 as [[->|contra]%limit_le_cases | Hunion1]. + { exfalso. by eapply (_ : Irreflexive limit_lt). } + { exfalso. by eapply (asymmetry (R:=limit_lt)). } + split; first done. by eapply limit_lt_le_trans. + - destruct Hunion as [Hunion1 Hunion2]. + apply limit_lt_max in Hunion2 as [Hunion2 | Hunion2]. + + apply limit_le_cases in Ht1 as [->|contra]. + { exfalso. by eapply (_ : Irreflexive limit_lt). } + { exfalso. by eapply (asymmetry (R:=limit_lt)). } + + split; last done. by trans e1. +Qed. + +Instance period_union_comm : Comm (=) period_union. +Proof. + unfold period_union. intros [s1 e1] [s2 e2]. + by rewrite limit_min_comm limit_max_comm. +Qed. + +Instance period_singleton : Singleton timestamp period := + λ t, [t, TsLimit (Z.succ t)). +Lemma period_singleton_lem_1 t : t ∈ ({[t]} : period). +Proof. by split; simpl; last lia. Qed. +Lemma period_singleton_lem_2 t t' : t' ∈ ({[t]} : period) → t' = t. +Proof. + unfold singleton, period_singleton. + intros [H11 H12]. destruct t, t'; try done; simpl in *; lia. +Qed. +Lemma period_singleton_nonempty t : period_nonempty {[t]}. +Proof. + apply period_nonempty_alt_iff. + exists t. apply period_singleton_lem_1. +Qed. + +Lemma unifiable_period_union p1 p2 p3 : + unifiable p1 p2 → unifiable p2 p3 → + unifiable (p1 ∪ p2) p3. +Proof. + destruct p1 as [s1 e1], p2 as [s2 e2], p3 as [s3 e3]. + intros [Hunif11 Hunif12] [Hunif21 Hunif22]. simpl. split. + + apply limit_le_max. by right. + + apply limit_min_le. by right. +Qed. + +Instance unifiable_symm : Symmetric unifiable. +Proof. by intros [] [] []. Qed. + +Definition Σlift {A} {Φ : A → Prop} (R : relation A) : relation {x : A | Φ x} := + λ '(x↾_) '(y↾_), R x y. + +Instance Σlift_symm {A Φ} `{!Symmetric R} : Symmetric (@Σlift A Φ R). +Proof. intros [x Hx] [y Hy] HR. simpl in *. by apply symmetry. Qed. + +(* TODO: probably unused? *) +Instance Σlift_dec {A Φ} `{!RelDecision R} : RelDecision (@Σlift A Φ R). +Proof. intros [x Hx] [y Hy]. by simpl. Qed. + +Definition ne_period_unifiable : relation ne_period := Σlift unifiable. + +Lemma period_unifiable_not_before p1 p2 : ne_period_unifiable p1 p2 → ¬ period_before p1 p2. +Proof. + destruct p1 as [[s1 e1] Hne1], p2 as [[s2 e2] Hne2]. simpl in *. + intros [Hunif1 Hunif2] Hbefore. + apply limit_le_cases in Hunif1 as [->|contra]. + - by eapply (_ : Irreflexive limit_lt). + - by eapply (asymmetry (R:=limit_lt)). +Qed. + +(* TODO: define total relation on Σperiod_nonempty, p1 p2 := unifiable p1 p2 ∨ p1 < p2. + (Then have [AntiSymm unifiable (≤@{Σperiod_nonempty})].) + Show decidability, perform mergesort. + Then make the rest of normalization consist in unification of the periods. + *) + +Definition ne_period_le p1 p2 := ne_period_unifiable p1 p2 ∨ period_before p1 p2. + +Instance ne_period_le_antisymm : AntiSymm ne_period_unifiable ne_period_le. +Proof. + intros p1 p2 [H12|H12] [H21|H21]; [done|done|..]. + - exfalso. apply symmetry in H21. + by eapply period_unifiable_not_before. + - exfalso. by eapply asymmetry. +Qed. + +Lemma ne_period_neither_before_unifiable p1 p2 : + ¬ period_before p1 p2 → ¬ period_before p2 p1 → + ne_period_unifiable p1 p2. +Proof. + destruct p1 as [[s1 e1] Hne1], p2 as [[s2 e2] Hne2]. + simpl in *. by intros ?%not_limit_lt ?%not_limit_lt. +Qed. + +Instance period_before_dec : RelDecision period_before. +Proof. + intros [[s1 e1] ?] [[s2 e2] ?]. simpl in *. + solve_decision. +Qed. + +Lemma ne_period_not_unifiable p1 p2 : + ¬ ne_period_unifiable p1 p2 → + period_before p1 p2 ∨ period_before p2 p1. +Proof. + intros Hnunif. + destruct (decide (period_before p1 p2)) as [?|H12]; first by left. + destruct (decide (period_before p2 p1)) as [?|H21]; first by right. + exfalso. by apply Hnunif, ne_period_neither_before_unifiable. +Qed. + +Instance ne_period_le_total : Total ne_period_le. +Proof. + intros p1 p2. + destruct (decide (ne_period_unifiable p1 p2)) as [Hunif|Hnunif]. + - (* which one we pick does not matter *) + by do 2 left. + - apply ne_period_not_unifiable in Hnunif as [H12|H21]. + + left. by right. + + right. by right. +Qed. diff --git a/server/formal/period_seq.v b/server/formal/period_seq.v new file mode 100644 index 0000000..705e505 --- /dev/null +++ b/server/formal/period_seq.v @@ -0,0 +1,834 @@ +From stdpp Require Import numbers option sorting ssreflect. +From stdpp Require Import options. +From routemon Require Import period util. + +(* This setup would require the proof irrelevance stuff + +Record period_seq := + PeriodSeq + { periods : list period + ; Hnonempty : Forall period_nonempty periods + ; Hsorted : Sorted period_before periods + }. +*) + +Definition period_seq := list ne_period. + +Definition period_seq_nf (ps : period_seq) := + Sorted period_before ps. + +Instance period_seq_elem_of : ElemOf timestamp period_seq := + λ t, Exists (λ p, t ∈ p). +Instance period_seq_elem_of_dec t (ps : period_seq) : Decision (t ∈ ps). +Proof. + induction ps as [|p ps]. + - right. inv 1. + - destruct IHps. + + left. by apply Exists_cons_tl. + + destruct (decide (t ∈ p)). + * left. by apply Exists_cons_hd. + * right. by inv 1. +Qed. + +Instance period_seq_equiv : Equiv period_seq := + λ ps1 ps2, ∀ t, t ∈ ps1 ↔ t ∈ ps2. + +Definition ne_period_intersection (p1 p2 : ne_period) := + let p := `p1 ∩ `p2 in + match decide (period_nonempty p) with + | left H => Some (p ↾ H) + | right _ => None + end. + +Definition ne_period_start (p : ne_period) := + period_start (`p). +Definition ne_period_end (p : ne_period) := + period_end (`p). + +Definition period_seq_intersection_1 go ps1 ps2 := + match ps1, ps2 with + | p1 :: ps1', p2 :: ps2' => + let mp12 := ne_period_intersection p1 p2 in + let rest := if decide (ne_period_end p1 < ne_period_end p2)%lim then go ps1' ps2 else go ps1 ps2' in + match mp12 with + | Some p12 => p12 :: rest + | None => rest + end + | _, _ => [] + end. +Fixpoint period_seq_intersection_aux n := + match n with + | 0 => const (const []) + | S n => period_seq_intersection_1 (period_seq_intersection_aux n) + end. +Instance period_seq_intersection : Intersection period_seq := + λ ps1 ps2, period_seq_intersection_aux (S (length ps1 + length ps2)) ps1 ps2. + +Lemma period_seq_intersection_eq ps1 ps2 : + period_seq_intersection ps1 ps2 = + match ps1, ps2 with + | p1 :: ps1', p2 :: ps2' => + let mp12 := ne_period_intersection p1 p2 in + let rest := if decide (ne_period_end p1 < ne_period_end p2)%lim + then period_seq_intersection ps1' ps2 + else period_seq_intersection ps1 ps2' in + match mp12 with + | Some p12 => p12 :: rest + | None => rest + end + | _, _ => [] + end. +Proof. + destruct ps1 as [|p1 ps1], ps2 as [|p2 ps2]; [done..|]. + have Hlen1 : S (S (length ps1 + length ps2)) = S (length (p1 :: ps1) + length ps2) by simpl; lia. + have Hlen2 : S (S (length ps1 + length ps2)) = S (length ps1 + length (p2 :: ps2)) by simpl; lia. + by rewrite + /period_seq_intersection /period_seq_intersection_aux + !length_cons Nat.add_succ_l Nat.add_succ_r -/period_seq_intersection_aux. +Qed. + +Opaque period_seq_intersection. + +Lemma period_seq_nf_cons p ps : + period_seq_nf (p :: ps) ↔ + period_seq_nf ps ∧ + Forall (λ q, ne_period_end p < ne_period_start q)%lim ps. +Proof. + split. + - intros HSort%Sorted_StronglySorted; last apply _. + inv HSort. repeat split; try done. + + by apply StronglySorted_Sorted. + + eapply Forall_impl; first done. + intros [[sq eq] Hq] Hbef. + by destruct p as [[sp ep] Hp]. + - intros (Hnf & Hlt). + constructor; first done. destruct ps as [|q ps]; constructor. + inv Hlt. destruct p as [[sp ep] Hp], q as [[sq eq] Hq]. by simpl in *. +Qed. + +(* +Lemma list_elem_of_cons_inv `{!EqDecision A} (x y : A) (l : list A) : + x ∈ y :: l ↔ x = y ∨ x ≠ y ∧ x ∈ l. +Proof. + split. + - destruct (decide (x = y)) as [<-|H]. + + intros _. by left. + + inv 1. by right. + - by intros [<-|[_ H]]; constructor. +Qed. +*) + +Lemma list_elem_of_cons_inv {A} (x y : A) (l : list A) : + x ∈ y :: l ↔ x = y ∨ x ∈ l. +Proof. + split. + - by inv 1; [left|right]. + - by intros [<-|H]; constructor. +Qed. + +Lemma Sorted_list_elem_of_R_trans {A} `{!Transitive R} (x y z : A) (l : list A) : + Sorted R (y :: l) → z ∈ y :: l → R x y → R x z. +Proof. + intros [HSort Hyl]%Sorted_inv. + revert y Hyl. + induction HSort as [|y' l' HSort IH Hy'l']; intros y. + - intros _. by inv 1; last inv H2. + - intros Hyy'%HdRel_inv. + intros [->|Hz]%list_elem_of_cons_inv; first done. + intros Hxy. + have : R x y' by eapply (_ : Transitive R). + by apply IH. +Qed. + +Lemma Sorted_list_elem_of_cons_inv {A} `{!Transitive R} (x y : A) (l : list A) : + Sorted R (y :: l) → + x ∈ y :: l → x = y ∧ Forall (R x) l ∨ R y x ∧ x ∈ l. +Proof. + intros HSort [->|H%list_elem_of_In]%list_elem_of_In%in_inv. + - left. by apply Sorted_StronglySorted in HSort as [_ ?]%StronglySorted_inv. + - right. inv HSort. inv H3; first inv H. + by split; first eapply Sorted_list_elem_of_R_trans. +Qed. + +Lemma period_seq_nf_elem_of_cons_inv (p1 p2 : ne_period) (ps : period_seq) : + period_seq_nf (p2 :: ps) → + p1 ∈ p2 :: ps → p1 = p2 ∧ Forall (period_before p1) ps ∨ + period_before p2 p1 ∧ p1 ∈ ps. +Proof. apply Sorted_list_elem_of_cons_inv. Qed. + +Inductive option_Exists {A} (Φ : A → Prop) : option A → Prop := + | Exists_Some (x : A) : Φ x → option_Exists Φ (Some x). + +Lemma option_Exists_from_option {A} Φ (mx : option A) : + option_Exists Φ mx ↔ from_option Φ False mx. +Proof. split; by [inv 1 | destruct mx]. Qed. + +Lemma period_seq_intersect_lem_aux (p1 p2 : ne_period) (ps1 ps2 : period_seq) : + is_Some (ne_period_intersection p1 p2) → + period_seq_nf ps1 → period_seq_nf ps2 → + p1 ∈ ps1 → p2 ∈ ps2 → + option_Exists (.∈ ps1 ∩ ps2) (ne_period_intersection p1 p2). +Proof. + intros Hne. revert ps2. + induction ps1 as [|[s1 e1] ps1]; first inv 3. + intros ps2 Hnf1 Hnf2 H1 H2. revert ps2 Hnf2 H2. + induction ps2 as [|[s2 e2] ps2]; first inv 2. + intros Hnf2 H2. + + apply option_Exists_from_option. + rewrite /intersection period_seq_intersection_eq /=. + apply period_seq_nf_elem_of_cons_inv in H1 as [[-> Hp1]|[Hlt1 H1]]; last done. + + apply period_seq_nf_elem_of_cons_inv in H2 as [[-> Hp2]|[Hlt2 H2]]; last done. + * unfold ne_period_intersection. + case_decide. + -- exfalso. simpl in Hne. + apply limit_le_cases in H as [contra|contra]. + ++ rewrite contra in Hne. by eapply (_ : Irreflexive limit_lt). + ++ by apply asymmetry in Hne. + -- constructor. + * case_decide. + -- case_decide. + ++ (* we need to show that [s1, e1) ## p2 *) + exfalso. assert ([s1, e1) ## p2). + { unfold disjoint, period_disjoint, period_empty. + destruct p2 as [s3 e3]. simpl. + destruct Hlt2 as [Hne2 [Hne3 Hlt2]]. + simpl in Hlt2. + + trans (e1 `min` e2)%lim. + { apply limit_le_cases. left. + trans e1. + - apply limit_min_eq_l, limit_le_cases. right. + by trans e2; last trans s3. + - apply symmetry, limit_min_eq_l, limit_le_cases. by right. } + etrans; first done. + apply limit_max_le. + split. + - apply limit_le_max. by left. + - apply limit_le_max. right. + apply limit_le_cases. right. by trans e2. } + by eapply period_empty_not_nonempty. + ++ by apply IHps2; first apply period_seq_nf_cons in Hnf2 as [_ [? _]]. + -- case_decide. + ++ apply not_limit_le in H. + (* [s1, e1) ## p2 since e1 < e2 and e2 < p2, but [s1, e1) ∩ p2 ≠ ∅ in hyp *) + exfalso. assert ([s1, e1) ## p2). + { unfold disjoint, period_disjoint, period_empty. + destruct p2 as [s3 e3]. simpl. + destruct Hlt2 as [Hne2 [Hne3 Hlt2]]. + simpl in Hlt2. + + rewrite limit_min_l; last first. + { apply limit_le_cases. right. + by trans e2; last trans s3. } + apply limit_le_max. right. + apply limit_le_cases. right. + by trans e2. } + by eapply period_empty_not_nonempty. + ++ constructor. by apply IHps2; first apply period_seq_nf_cons in Hnf2 as (_ & ? & _). + + apply period_seq_nf_elem_of_cons_inv in H2 as [[-> Hp2]|[Hlt2 H2]]; last done. + * case_decide. + -- case_decide. + ++ apply IHps1; try done. + ** by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + ** by constructor. + ++ apply not_limit_lt in H0. exfalso. assert (p1 ## [s2, e2)). + { unfold disjoint, period_disjoint, period_empty. + destruct p1 as [s3 e3]. simpl. + destruct Hlt1 as [Hne1 [Hne3 Hlt1]]. + simpl in Hlt1. + + trans (e1 `min` e2)%lim. + { apply limit_le_cases. left. + trans e2. + - apply limit_min_eq_r. trans e1; first done. + apply limit_le_cases. right. by trans s3. + - by apply symmetry, limit_min_eq_r. } + etrans; first done. + apply limit_max_le. + split. + - apply limit_le_max. left. + apply limit_le_cases. right. by trans e1. + - apply limit_le_max. by right. } + by eapply period_empty_not_nonempty. + -- case_decide. + ++ constructor. apply IHps1; try done. + ** by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + ** by constructor. + ++ apply not_limit_lt in H0. apply not_limit_le in H. + exfalso. assert (p1 ## [s2, e2)). + { unfold disjoint, period_disjoint, period_empty. + destruct p1 as [s3 e3]. simpl. + destruct Hlt1 as [Hne1 [Hne3 Hlt1]]. + simpl in Hlt1. + + rewrite limit_min_r; last first. + { trans e1; first done. + apply limit_le_cases. right. + by trans s3. } + trans e1; first done. + apply limit_le_max. left. + apply limit_le_cases. by right. } + by eapply period_empty_not_nonempty. + * case_decide. + -- case_decide. + ++ apply IHps1; try done. + ** by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + ** by constructor. + ++ by apply IHps2; first apply period_seq_nf_cons in Hnf2 as (_ & ? & _). + -- case_decide; constructor. + ++ apply IHps1; try done. + ** by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + ** by constructor. + ++ by apply IHps2; first apply period_seq_nf_cons in Hnf2 as (_ & ? & _). +Qed. + +Lemma period_seq_intersection_inv (p : period) (ps1 ps2 : period_seq) : + period_seq_nf ps1 → period_seq_nf ps2 → p ∈ ps1 ∩ ps2 → + ∃ p1 p2, p1 ∈ ps1 ∧ p2 ∈ ps2 ∧ p = p1 ∩ p2. +Proof. + intros Hnf1. revert ps2. + induction ps1; first inv 2. + induction ps2. + { intros _ contra. + rewrite period_seq_intersection_eq in contra. + destruct a. inv contra. } + destruct a as [s1 e1], a0 as [s2 e2]. + intros Hnf2 Hint. + rewrite period_seq_intersection_eq in Hint. + simpl in Hint. case_decide; case_decide. + - apply IHps1 in Hint as (q1 & q2 & Hq1 & Hq2 & ->); last done. + + exists q1, q2. by repeat split; first constructor. + + by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + - apply IHps2 in Hint as (q1 & q2 & Hq1 & Hq2 & ->). + + exists q1, q2. by repeat split; last constructor. + + by apply period_seq_nf_cons in Hnf2 as (_ & ? & _). + - inv Hint. + + exists [s1, e1), [s2, e2). repeat split; constructor. + + apply IHps1 in H3 as (q1 & q2 & Hq1 & Hq2 & ->); last done. + * exists q1, q2. by repeat split; first constructor. + * by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + - inv Hint. + + exists [s1, e1), [s2, e2). repeat split; constructor. + + apply IHps2 in H3 as (q1 & q2 & Hq1 & Hq2 & ->). + * exists q1, q2. by repeat split; last constructor. + * by apply period_seq_nf_cons in Hnf2 as (_ & ? & _). +Qed. + +Lemma period_seq_intersection_lem t (ps1 ps2 : period_seq) : + period_seq_nf ps1 → period_seq_nf ps2 → + t ∈ ps1 ∧ t ∈ ps2 ↔ t ∈ ps1 ∩ ps2. +Proof. + intros Hnf1 Hnf2. + split. + - intros [H1 H2]. + unfold elem_of, period_seq_elem_of in H1, H2. + apply Exists_exists in H1 as (p1 & Hp1 & Ht1). + apply Exists_exists in H2 as (p2 & Hp2 & Ht2). + assert (Ht : t ∈ p1 ∩ p2). { by apply intersect_and. } + clear Ht1 Ht2. + unfold elem_of, period_seq_elem_of. + apply Exists_exists. exists (p1 ∩ p2). + split; first apply period_seq_intersect_lem_aux; try done. + apply period_nonempty_alt_iff. by exists t. + - intros (p & Hp & Ht)%Exists_exists. + apply period_seq_intersection_inv in Hp as (p1 & p2 & Hp1 & Hp2 & ->); try done. + apply intersect_and in Ht as [Ht1 Ht2]. + split; apply Exists_exists; by eexists. +Qed. + +Definition period_seq_extent (ps : period_seq) : period := + match head ps, last ps with + | Some [s, _), Some [_, e) => [s, e) + | _, _ => ∅ + end. + +(* +Lemma period_seq_extent_hd ps : + period_seq_nf ps → + Forall (period_start (period_seq_extent ps) + +Lemma period_seq_extent_spec t ps : + period_seq_nf ps → t ∈ ps → + t ∈ period_seq_extent ps. +Proof. + Search StronglySorted. + induction ps as [|p ps]; first inv 2. + intros Hnf. inv 1. + - + +Qed. +*) + +(* +Definition period_seq_intersection_extent (ps1 ps2 : period_seq) : + period_seq_nf ps1 → period_seq_nf ps2 → + period_seq_extent (ps1 ∩ ps2) = period_seq_extent ps1 ∩ period_seq_extent ps2. +Proof. + intros Hnf1 Hnf2. + destruct (decide (period_empty (period_seq_extent (ps1 ∩ ps2)))). + - admit. + - apply period_empty_not_nonempty in n. + apply period_nonempty_equiv_L; first done. + + admit. + + intros t. + Search period equiv eq. +*) + +(* The intersection preserves normal forms *) +Lemma period_seq_intersection_nf (ps1 ps2 : period_seq) : + period_seq_nf ps1 → period_seq_nf ps2 → period_seq_nf (ps1 ∩ ps2). +Proof. + intros Hnf1. revert ps2. + induction ps1 as [|[s1 e1] ps1]; induction ps2 as [|[s2 e2] ps2]; [done..|]. + intros Hnf2. rewrite period_seq_intersection_eq /=. + case_decide. + - case_decide. + + by apply IHps1; first apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + + apply IHps2. by apply period_seq_nf_cons in Hnf2 as (_ & ? & _). + - case_decide. + + apply IHps1 in Hnf2 as Hnf2'; last by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + destruct Hnf2' as [Hne HSort]. split. + * by constructor; first apply period_empty_not_nonempty. + * constructor; first done. + rewrite {1}/intersection /period_intersection. + apply period_seq_nf_cons in Hnf1 as (Hne1 & Hnf1 & Hlt1). + destruct (ps1 ∩ ([s2, e2) :: ps2)) as [|[sq eq] qs] eqn:Hqs; constructor. + assert (Hq : [sq, eq) ∈ ps1 ∩ ([s2, e2) :: ps2)). + { rewrite Hqs. constructor. } + unfold period_before. repeat split. + -- by apply period_empty_not_nonempty. + -- by eapply Forall_forall; first apply Hne. + -- specialize (IHps1 Hnf1). + apply period_seq_intersection_inv in Hq as ([sq1 eq1] & [sq2 eq2] & Hq1%list_elem_of_In & Hq2 & Hq); [|done..]. + apply (proj1 (List.Forall_forall _ _) Hlt1) in Hq1. + injection Hq as -> ->. + simplify_eq/=. + apply limit_min_lt. left. + apply limit_lt_max. by left. + + apply IHps1 in Hnf2 as Hnf2'; last by apply period_seq_nf_cons in Hnf1 as (_ & ? & _). + destruct Hnf2' as [Hne HSort]. + apply not_limit_lt in H0. split. + * constructor. + -- by apply period_empty_not_nonempty. + -- apply IHps2. by apply period_seq_nf_cons in Hnf2 as (_ & ? & _). + * constructor. + -- apply IHps2. by apply period_seq_nf_cons in Hnf2 as (_ & ? & _). + -- rewrite {1}/intersection /period_intersection. + apply period_seq_nf_cons in Hnf1 as Hnf1'. + destruct Hnf1' as (Hp1 & Hnf1' & Hne1). + apply period_seq_nf_cons in Hnf2 as (Hne2 & Hnf2 & Hlt2). + apply IHps2 in Hnf2 as Hnf2'. + destruct (([s1, e1) :: ps1) ∩ ps2) as [|[sq eq] qs] eqn:Hqs; constructor. + assert (Hq : [sq, eq) ∈ ([s1, e1) :: ps1) ∩ ps2). + { rewrite Hqs. constructor. } + unfold period_before. repeat split. + ++ by apply period_empty_not_nonempty. + ++ by eapply Forall_forall; first apply Hnf2'. + ++ apply period_seq_intersection_inv in Hq as ([sq1 eq1] & [sq2 eq2] & Hq1 & Hq2%list_elem_of_In & Hq); [|done..]. + apply (proj1 (List.Forall_forall _ _) Hlt2) in Hq2. + injection Hq as -> ->. + simplify_eq/=. + apply limit_min_lt. right. + apply limit_lt_max. by right. +Qed. + +Definition period_seq_intersection_comm_equiv ps1 ps2 : + period_seq_nf ps1 → period_seq_nf ps2 → + ps1 ∩ ps2 ≡ ps2 ∩ ps1. +Proof. + intros Hnf1 Hnf2 t. + split; by intros [H2 H1]%period_seq_intersection_lem; + first apply period_seq_intersection_lem. +Qed. + +Lemma period_seq_nf_cons_equiv_inv_start_1 p1 ps1 p2 ps2: + period_seq_nf (p1 :: ps1) → + period_seq_nf (p2 :: ps2) → + p1 :: ps1 ≡ p2 :: ps2 → + ¬ (period_start p1 < period_start p2)%lim. +Proof. + intros Hnf1 Hnf2 Hequiv Hp12. + apply period_seq_nf_cons in Hnf1 as (Hne1 & Hnf1 & Hlt1), Hnf2 as (Hne2 & Hnf2 & Hlt2). + destruct p1 as [s1 e1], p2 as [s2 e2]. simpl in Hp12. + destruct s2 as [|s2|]; [by destruct s1| |done]. + destruct s1 as [|s1|]; last done. + - destruct e1 as [|e1|]; first done. + + assert (Z.pred (s2 `min` e1) ∈ [-∞, e1) :: ps1). + { constructor. by split; [|simpl; lia]. } + apply Hequiv in H. inv H. + * destruct H1 as [H1 _]. simpl in H1. lia. + * apply Exists_exists in H1 as ([sp ep] & Hp & Hs2). + rewrite Forall_forall in Hlt2. + apply Hlt2 in Hp. simpl in Hp. + destruct Hs2 as [Hs21 Hs22]. + assert (contra : (s2 < s2)%lim). + { trans e2; first done. + apply (limit_lt_le_trans sp); first done. + etrans; first apply Hs21. simpl. lia. } + by eapply (_ : Irreflexive limit_lt). + + assert (Z.pred s2 ∈ [-∞, +∞) :: ps1). + { by constructor. } + apply Hequiv in H. inv H. + * destruct H1 as [H1 _]. simpl in H1. lia. + * apply Exists_exists in H1 as ([sp ep] & Hp & Hs2). + rewrite Forall_forall in Hlt2. + apply Hlt2 in Hp. simpl in Hp. + destruct Hs2 as [Hs21 Hs22]. + assert (contra : (s2 < s2)%lim). + { trans e2; first done. + apply (limit_lt_le_trans sp); first done. + etrans; first apply Hs21. simpl. lia. } + by eapply (_ : Irreflexive limit_lt). + - assert (s1 ∈ [s1, e1) :: ps1). + { by constructor. } + apply Hequiv in H. inv H. + + destruct H1 as [[[= ->]|H1]%limit_le_cases _]. + * by eapply (_ : Irreflexive limit_lt). + * by eapply (asymmetry (R:=limit_lt)). + + apply Exists_exists in H1 as ([sp ep] & Hp & Hs1). + rewrite Forall_forall in Hlt2. + apply Hlt2 in Hp. simpl in Hp. + assert (contra : (s1 < s1)%lim). + { trans s2; first done. + trans e2; first done. + by apply (limit_lt_le_trans sp); last apply Hs1. } + by eapply (_ : Irreflexive limit_lt). +Qed. + +Instance period_seq_equiv_trans : Transitive (≡@{period_seq}). +Proof. + intros ps1 ps2 ps3 Heq12 Heq23 t. split. + - by intros Ht%Heq12%Heq23. + - by intros Ht%Heq23%Heq12. +Qed. + +Instance period_seq_equiv_symm : Symmetric (≡@{period_seq}). +Proof. intros ps1 ps2 Heq12 t. split; by intros Ht%Heq12. Qed. + +Lemma period_seq_nf_cons_equiv_inv_start p1 ps1 p2 ps2: + period_seq_nf (p1 :: ps1) → + period_seq_nf (p2 :: ps2) → + p1 :: ps1 ≡ p2 :: ps2 → + period_start p1 = period_start p2. +Proof. + intros Hnf1 Hnf2 Hequiv. + destruct (decide (period_start p1 < period_start p2)%lim) as [Hs12|Hs21]. + - exfalso. by eapply period_seq_nf_cons_equiv_inv_start_1 in Hs12. + - apply not_limit_lt, limit_le_cases in Hs21 as [Hs21|Hs21]; first done. + exfalso. by eapply period_seq_nf_cons_equiv_inv_start_1 in Hs21. +Qed. + +Lemma period_seq_nf_cons_equiv_inv_end_1 p1 ps1 p2 ps2: + period_seq_nf (p1 :: ps1) → + period_seq_nf (p2 :: ps2) → + p1 :: ps1 ≡ p2 :: ps2 → + ¬ (period_end p1 < period_end p2)%lim. +Proof. + intros Hnf1 Hnf2 Hequiv Hp12. + assert (Hs : period_start p1 = period_start p2). + { by eapply period_seq_nf_cons_equiv_inv_start. } + apply period_seq_nf_cons in Hnf1 as (Hne1 & Hnf1 & Hlt1), Hnf2 as (Hne2 & Hnf2 & Hlt2). + destruct p1 as [s1 e1], p2 as [s2 e2]. simpl in Hs, Hp12. + rewrite <-Hs in *. rename s1 into s. clear Hs s2. + destruct e1 as [|e1|]; [by destruct s| |done]. + assert (e1 ∈ [s, e2) :: ps2). + { constructor. by split; [apply limit_le_cases; right|]. } + apply Hequiv in H. inv H. + + destruct H1 as [_ H12]. by eapply (_ : Irreflexive limit_lt). + + apply Exists_exists in H1 as (p & Hp & He1). + rewrite Forall_forall in Hlt1. + apply Hlt1 in Hp. + destruct p as [sp ep]. + unfold period_end, period_start in Hp. + assert (contra : (e1 < e1)%lim). + { by eapply limit_lt_le_trans; last apply He1. } + by eapply (_ : Irreflexive limit_lt). +Qed. + +Lemma period_seq_nf_cons_equiv_inv_end p1 ps1 p2 ps2: + period_seq_nf (p1 :: ps1) → + period_seq_nf (p2 :: ps2) → + p1 :: ps1 ≡ p2 :: ps2 → + period_end p1 = period_end p2. +Proof. + intros Hnf1 Hnf2 Hequiv. + destruct (decide (period_end p1 < period_end p2)%lim) as [He12|He21]. + - exfalso. by eapply period_seq_nf_cons_equiv_inv_end_1 in He12. + - apply not_limit_lt, limit_le_cases in He21 as [He21|He21]; first done. + exfalso. by eapply period_seq_nf_cons_equiv_inv_end_1 in He21. +Qed. + +Lemma period_seq_nf_cons_equiv_inv p1 ps1 p2 ps2: + period_seq_nf (p1 :: ps1) → + period_seq_nf (p2 :: ps2) → + p1 :: ps1 ≡ p2 :: ps2 → + p1 = p2. +Proof. + intros Hnf1 Hnf2 Hequiv. + trans [period_start p1, period_end p1); first by destruct p1. + trans [period_start p2, period_end p2); last by destruct p2. + erewrite period_seq_nf_cons_equiv_inv_start; try done. + by erewrite period_seq_nf_cons_equiv_inv_end. +Qed. + +Lemma period_seq_nf_equiv_L ps1 ps2 : + period_seq_nf ps1 → + period_seq_nf ps2 → + ps1 ≡ ps2 → ps1 = ps2. +Proof. + intros Hnf1. revert ps2. + induction ps1 as [|p1 ps1]; intros ps2 Hnf2 Hequiv. + - destruct ps2; first done. + assert (period_nonempty p) as [t Ht]%period_nonempty_alt_iff. + { inv Hnf2. by inv H. } + assert (t ∈ p :: ps2) as contra%Hequiv. + { by apply Exists_cons_hd. } + inv contra. + - destruct ps2 as [|p2 ps2]. + + assert (period_nonempty p1) as [t Ht]%period_nonempty_alt_iff. + { inv Hnf1. by inv H. } + assert (t ∈ p1 :: ps1) as contra%Hequiv. + { by apply Exists_cons_hd. } + inv contra. + + assert (p1 = p2) as <-. + { by eapply period_seq_nf_cons_equiv_inv. } + rename p1 into p. + apply period_seq_nf_cons in Hnf1 as (Hne1 & Hnf1 & Hlt1), Hnf2 as (Hne2 & Hnf2 & Hlt2). + f_equal. apply IHps1; [done..|]. + intros t. split; intros Ht. + * assert (t ∈ p :: ps1) as H%Hequiv. + { by apply Exists_cons_tl. } + inv H; last done. + apply Exists_exists in Ht as ([sq eq] & Hp & Ht). + rewrite Forall_forall in Hlt1. + apply Hlt1 in Hp. + exfalso. destruct p as [s e]. + simpl in *. + assert (contra : (t < t)%lim). + { trans e; first apply H1. + by eapply limit_lt_le_trans; last apply Ht. } + by eapply (_ : Irreflexive limit_lt). + * assert (t ∈ p :: ps2) as H%Hequiv. + { by apply Exists_cons_tl. } + inv H; last done. + apply Exists_exists in Ht as ([sq eq] & Hp & Ht). + rewrite Forall_forall in Hlt2. + apply Hlt2 in Hp. + exfalso. destruct p as [s e]. + simpl in *. + assert (contra : (t < t)%lim). + { trans e; first apply H1. + by eapply limit_lt_le_trans; last apply Ht. } + by eapply (_ : Irreflexive limit_lt). +Qed. + +Definition period_seq_intersection_comm ps1 ps2 : + period_seq_nf ps1 → period_seq_nf ps2 → + ps1 ∩ ps2 = ps2 ∩ ps1. +Proof. + intros Hnf1 Hnf2. + apply period_seq_nf_equiv_L. + - by apply period_seq_intersection_nf. + - by apply period_seq_intersection_nf. + - by apply period_seq_intersection_comm_equiv. +Qed. + +Instance period_seq_equiv_refl : Reflexive (≡@{period_seq}). +Proof. done. Qed. + +Instance period_seq_equiv_equivalence : Equivalence (≡@{period_seq}). +Proof. split; apply _. Qed. + +Variant bound := + | LtBound of limit + | GeBound of limit. + +Definition bound_le b1 b2 := + match b1, b2 with + | GeBound l1, GeBound l2 => (l1 ≤ l2)%lim + | GeBound _, LtBound _ => True + | LtBound l1, LtBound l2 => (l1 ≤ l2)%lim + | LtBound _, GeBound _ => False + end. +Instance bound_lt_dec : RelDecision bound_le. +Proof. intros [l1|l1] [l2|l2]; simpl; solve_decision. Qed. + +Instance bound_le_refl : Reflexive bound_le. +Proof. by intros []; simpl. Qed. + +Instance bound_le_trans : Transitive bound_le. +Proof. intros [] [] [] ? ?; simpl in *; done || by etrans. Qed. + +Instance bound_le_preorder : PreOrder bound_le. +Proof. split; apply _. Qed. + +Instance bound_le_antisymm : AntiSymm (=) bound_le. +Proof. intros [] [] ? ?; simpl in *; done || f_equal; by eapply (_ : AntiSymm (=) limit_le). Qed. + +Instance bound_le_partial_order : PartialOrder bound_le. +Proof. split; apply _. Qed. + +Instance bound_le_trichotomy : Trichotomy (strict bound_le). +Proof. + intros [] []; simpl in *. + - destruct (trichotomy _ l l0) as [?|[?|?]]. + + left. split; simpl. + * apply limit_le_cases. by right. + * by apply not_limit_le. + + right. left. by subst. + + right. right. split; simpl. + * apply limit_le_cases. by right. + * by apply not_limit_le. + - right. right. split; simpl; [done|by intros ?]. + - left. split; simpl; [done|by intros ?]. + - destruct (trichotomy _ l l0) as [?|[?|?]]. + + left. split; simpl. + * apply limit_le_cases. by right. + * by apply not_limit_le. + + right. left. by subst. + + right. right. split; simpl. + * apply limit_le_cases. by right. + * by apply not_limit_le. +Qed. + +Instance bound_le_total_order : TotalOrder bound_le. +Proof. split; apply _. Qed. + +Definition period_bounds '[s, e) := + if decide (period_nonempty [s, e)) then [GeBound s; LtBound e] else []. + +Definition period_seq_bounds (ps : period_seq) := + ps ≫= period_bounds. + +Definition period_seq_bounds_sorted (ps : period_seq) := + merge_sort bound_le (period_seq_bounds ps). + +Variant window_filter_action := + KickLeft | KickRight | NoAction. +Fixpoint window_filter_aux {A} (f : A → A → window_filter_action) (x : A) (l : list A) := + match l with + | [] => [x] + | y :: l' => + match f x y with + | KickLeft => window_filter_aux f y l' + | KickRight => window_filter_aux f x l' + | NoAction => x :: window_filter_aux f y l' + end + end. +Definition window_filter {A} (f : A → A → window_filter_action) (l : list A) := + match l with + | [] => [] + | x :: l' => window_filter_aux f x l' + end. + +Definition period_seq_bounds_clean (ps : period_seq) := + window_filter (λ b1 b2, match b1, b2 with + | GeBound _, LtBound _ => NoAction + | GeBound _, GeBound _ => KickRight + | LtBound _, GeBound _ => NoAction + | LtBound _, LtBound _ => KickLeft + end) + (period_seq_bounds_sorted ps). + +Fixpoint period_seq_from_bounds (bs : list bound) : period_seq := + match bs with + | GeBound s :: LtBound e :: bs' => [s, e) :: period_seq_from_bounds bs' + | _ => [] + end. + +Definition period_seq_normalize (ps : period_seq) := + period_seq_from_bounds (period_seq_bounds_clean ps). + + +Lemma period_seq_normalize_lem_1 (ps : period_seq) : + period_seq_normalize ps ≡ ps. +Proof. + Search merge_sort. + Search Total Trichotomy. + + +(* TODO: continue here *) Admitted. + +Lemma period_seq_normalize_lem_2 (ps : period_seq) : + period_seq_nf (period_seq_normalize ps). +Proof. (* TODO: and here *) Admitted. + +Definition period_seq_union (ps1 ps2 : period_seq) := + period_seq_normalize (ps1 ++ ps2). +Lemma period_seq_union_lem t ps1 ps2 : + t ∈ period_seq_union ps1 ps2 ↔ t ∈ ps1 ∨ t ∈ ps2. +Proof. + split. + - intros Ht%(period_seq_normalize_lem_1 (ps1 ++ ps2)). + apply Exists_app in Ht as [Ht|Ht]; by [left|right]. + - intros [Ht|Ht]; apply period_seq_normalize_lem_1, Exists_app; by [left|right]. +Qed. +Lemma period_seq_union_nf ps1 ps2 : + period_seq_nf (period_seq_union ps1 ps2). +Proof. apply period_seq_normalize_lem_2. Qed. + +Definition nf_period_seq := sig period_seq_nf. + +Instance period_seq_nf_pi ps : ProofIrrel (period_seq_nf ps). +Proof. + unfold period_seq_nf. intros [P11 P12] [P21 P22]. + f_equal; [apply Forall_pi | apply Sorted_pi]; apply _. +Qed. + +Instance period_seq_empty : Empty period_seq := []. +Lemma period_seq_empty_nf : period_seq_nf ∅. +Proof. done. Qed. + +Instance period_seq_singleton : Singleton timestamp period_seq := + λ t, [{[t]}]. +Lemma period_seq_singleton_lem_1 t : t ∈ ({[t]} : period_seq). +Proof. + unfold singleton, period_seq_singleton. + constructor. apply period_singleton_lem_1. +Qed. +Lemma period_seq_singleton_lem_2 t t' : t' ∈ ({[t]} : period_seq) → t' = t. +Proof. + unfold singleton, period_seq_singleton. + inv 1; last inv H1. + by apply period_singleton_lem_2. +Qed. +Lemma period_seq_singleton_nf t : period_seq_nf {[t]}. +Proof. + unfold singleton, period_seq_singleton. + split. + - constructor; last constructor. + apply period_singleton_nonempty. + - constructor; constructor. +Qed. + +Instance nf_period_seq_elem_of : ElemOf timestamp nf_period_seq := + λ t ps, t ∈ `ps. +Instance nf_period_seq_empty : Empty nf_period_seq := + ∅ ↾ period_seq_empty_nf. +Instance nf_period_seq_union : Union nf_period_seq := + λ '(ps1↾_) '(ps2↾_), period_seq_union ps1 ps2 ↾ (period_seq_union_nf ps1 ps2). +Instance nf_period_seq_singleton : Singleton timestamp nf_period_seq := + λ t, {[t]} ↾ period_seq_singleton_nf t. + +Instance nf_period_seq_semiset : SemiSet timestamp nf_period_seq. +Proof. + split. + - intros t Ht. inv Ht. + - split. + + apply period_seq_singleton_lem_2. + + intros <-. apply period_seq_singleton_lem_1. + - intros [ps1 Hnf1] [ps2 Hnf2] t. + unfold union, nf_period_seq_union, elem_of, nf_period_seq_elem_of. + simpl. apply period_seq_union_lem. +Qed. + +Instance nf_period_seq_intersection : Intersection nf_period_seq := + λ '(ps1↾Hnf1) '(ps2↾Hnf2), (ps1 ∩ ps2) ↾ (period_seq_intersection_nf ps1 ps2 Hnf1 Hnf2). + +(* TODO: difference! + +Instance nf_period_seq_set : Set_ timestamp nf_period_seq. +Proof. (* TODO *) Qed. + +*) diff --git a/server/formal/util.v b/server/formal/util.v new file mode 100644 index 0000000..8e16ffc --- /dev/null +++ b/server/formal/util.v @@ -0,0 +1,92 @@ +From stdpp Require Import numbers option sorting ssreflect. +From stdpp Require Import options. + +Definition transportf {X} (P : X → Type) {x x' : X} : x = x' → P x → P x'. +Proof. by induction 1. Defined. + +Instance HdRel_pi {A} (R : relation A) `{!EqDecision A} `{!∀ x y, ProofIrrel (R x y)} a l : ProofIrrel (HdRel R a l). +Proof. + intros HR1 HR2. + assert (Hnil : ∀ xs (Hxs : [] = xs) (HR : HdRel R a xs), + HR = transportf _ Hxs (HdRel_nil R a)). + { intros. destruct HR; last done. + by replace Hxs with (eq_refl ([] : list A)); last apply eq_pi, list_eq_dec. } + assert (Hcons : ∀ xs x y xs' (Hxs : y :: xs' = xs) (HR : HdRel R x xs) (Hxy : R x y), + HR = transportf (HdRel R x) Hxs (HdRel_cons R x y xs' Hxy)). + { intros. destruct HR; first done. + injection Hxs as <- <-. + replace Hxs with (eq_refl (y :: xs')); last apply eq_pi, list_eq_dec. + simpl. + by replace r with Hxy by apply H. } + destruct l. + - trans (transportf (HdRel R a) eq_refl (HdRel_nil R a)). + + apply Hnil. + + symmetry. apply Hnil. + - apply HdRel_inv in HR1 as Haa0. + trans (transportf (HdRel R a) eq_refl (HdRel_cons R a a0 l Haa0)). + + apply Hcons. + + symmetry. apply Hcons. +Qed. + +Instance Sorted_pi {A} (R : relation A) `{!EqDecision A} `{!∀ x y, ProofIrrel (R x y)} l : ProofIrrel (Sorted R l). +Proof. + intros HS1 HS2. + assert (Hbase : ∀ xs (Hxs : [] = xs) (HS : Sorted R xs), HS = transportf _ Hxs (Sorted_nil R)). + { intros. destruct HS; last done. + by replace Hxs with (eq_refl ([] : list A)); last apply eq_pi, list_eq_dec. } + assert (Hind : ∀ xs x xs' + (Hxs : x :: xs' = xs) (HS : Sorted R xs) + (Hx : HdRel R x xs') (HS' : Sorted R xs') + (IH : ∀ HS1' HS2' : Sorted R xs', HS1' = HS2'), + HS = transportf _ Hxs (Sorted_cons HS' Hx)). + { intros. destruct HS; first done. + injection Hxs as <- <-. + replace Hxs with (eq_refl (x :: xs')); last apply eq_pi, list_eq_dec. + simpl. + replace h with Hx by apply: HdRel_pi. + by replace HS with HS' by apply IH. } + induction l. + - trans (transportf _ eq_refl (Sorted_nil R)). + + apply Hbase. + + symmetry. apply Hbase. + - destruct (Sorted_inv HS1) as [Hl Hal]. + trans (transportf _ eq_refl (Sorted_cons Hl Hal)). + + apply Hind, IHl. + + symmetry. apply Hind, IHl. +Qed. + +Instance Forall_pi {A} (P : A → Prop) (l : list A) `{!EqDecision A} `{!∀ a, ProofIrrel (P a)} : ProofIrrel (Forall P l). +Proof. + intros HF1 HF2. + assert (Hbase : ∀ xs (Hxs : [] = xs) (HF : Forall P xs), HF = transportf (Forall P) Hxs (ListDef.Forall_nil P)). + { intros xs Hxs HF. destruct HF; last done. + by replace Hxs with (eq_refl ([] : list A)); last apply eq_pi, list_eq_dec. } + assert (Hind : ∀ xs x xs' + (Hxs : x :: xs' = xs) (HF : Forall P xs) + (Hx : P x) (HF' : Forall P xs') + (IH : ∀ HF1' HF2' : Forall P xs', HF1' = HF2'), + HF = transportf _ Hxs (ListDef.Forall_cons P x xs' Hx HF')). + { intros. destruct HF; first done. + injection Hxs as <- <-. + replace Hxs with (eq_refl (x :: xs')); last apply eq_pi, list_eq_dec. + simpl. + replace p with Hx by apply H. + by replace HF with HF' by apply IH. } + induction l. + - trans (transportf _ eq_refl (ListDef.Forall_nil P)). + + apply Hbase. + + symmetry. apply Hbase. + - apply Forall_inv in HF1 as Ha. + apply Forall_inv_tail in HF1 as Hl. + trans (transportf _ eq_refl (ListDef.Forall_cons P a l Ha Hl)). + + apply Hind, IHl. + + symmetry. apply Hind, IHl. +Qed. + +Instance ex_pi {A} {B : A → Prop} `{!ProofIrrel A} `{!∀ x, ProofIrrel (B x)} : + ProofIrrel (∃ (x : A), B x). +Proof. + intros [x Hx] [y Hy]. + assert (y = x) by apply proof_irrel. subst. + assert (Hx = Hy) by apply proof_irrel. by subst. +Qed. diff --git a/server/hack/rwgps-get-auth-token.sh b/server/hack/rwgps-get-auth-token.sh new file mode 100755 index 0000000..d7a14a5 --- /dev/null +++ b/server/hack/rwgps-get-auth-token.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +rwgps_api_key="$(jq +# This file is distributed under the same license as the routemon package. +# Rutger Broekhoff , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-28 09:20+0200\n" +"PO-Revision-Date: 2026-08-28 14:31+0200\n" +"Last-Translator: Rutger Broekhoff \n" +"Language-Team: English\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../src/http_server.cppm:232 +msgid "No body expected for this request" +msgstr "No body expected for this request" + +#: ../src/http_server.cppm:500 +msgid "Bad request" +msgstr "Bad request" + +#: ../src/http_server.cppm:511 ../src/http_server.cppm:562 +msgid "Method not allowed" +msgstr "Method not allowed" + +#: ../src/http_server.cppm:527 +msgid "" +"Path of normalized (RFC 3986, § 6) origin-form request-target (RFC 9112, § " +"3.2.1) should be absolute" +msgstr "Path of normalized (RFC 3986, § 6) origin-form request-target (RFC 9112, § 3.2.1) should be absolute" + +#: ../src/http_server.cppm:538 +msgid "Not found" +msgstr "Not found" + +#: ../src/http_server.cppm:549 +msgid "Method not implemented" +msgstr "Method not implemented" + +#: ../src/http_server.cppm:573 +msgid "" +"Invalid request-target, expected asterisk-form or origin-form (see RFC 9112, " +"§ 3.2)" +msgstr "Invalid request-target, expected asterisk-form or origin-form (see RFC 9112, § 3.2)" + +#: ../src/srv.cppm:148 +msgid "Failed to parse GPX file" +msgstr "Failed to parse GPX file" + +#: ../src/srv.cppm:160 +msgid "Internal server error" +msgstr "Internal server error" + +#~ msgid "Invalid point in GPX file" +#~ msgstr "Invalid point in GPX file" + +#~ msgid "GPX file has no points in track" +#~ msgstr "GPX file has no points in track" diff --git a/server/locale/nl.po b/server/locale/nl.po new file mode 100644 index 0000000..96b6cd9 --- /dev/null +++ b/server/locale/nl.po @@ -0,0 +1,64 @@ +# Translations for the routemon project. +# Copyright (C) 2026 Rutger Broekhoff +# This file is distributed under the same license as the routemon package. +# Rutger Broekhoff , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: routemon 0.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-28 09:20+0200\n" +"PO-Revision-Date: 2026-08-28 14:35+0200\n" +"Last-Translator: Rutger Broekhoff \n" +"Language-Team: Dutch \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../src/http_server.cppm:232 +msgid "No body expected for this request" +msgstr "Geen body verwacht voor deze aanvraag" + +#: ../src/http_server.cppm:500 +msgid "Bad request" +msgstr "Ongeldige aanvraag" + +#: ../src/http_server.cppm:511 ../src/http_server.cppm:562 +msgid "Method not allowed" +msgstr "Aanvraagmethode niet toegestaan" + +#: ../src/http_server.cppm:527 +msgid "" +"Path of normalized (RFC 3986, § 6) origin-form request-target (RFC 9112, § " +"3.2.1) should be absolute" +msgstr "Pad van genormaliseerde (RFC 3986, § 6) origin-form request-target (RFC 9112, § 3.2.1) behoort absoluut te zijn" + +#: ../src/http_server.cppm:538 +msgid "Not found" +msgstr "Niet gevonden" + +#: ../src/http_server.cppm:549 +msgid "Method not implemented" +msgstr "Aanvraagmethode niet geïmplementeerd" + +#: ../src/http_server.cppm:573 +msgid "" +"Invalid request-target, expected asterisk-form or origin-form (see RFC 9112, " +"§ 3.2)" +msgstr "Ongeldige request-target, asterisk-form of origin-form verwacht (zie RFC 9112, § 3.2)" + +#: ../src/srv.cppm:148 +msgid "Failed to parse GPX file" +msgstr "GPX-bestand kon niet geladen worden" + +#: ../src/srv.cppm:160 +msgid "Internal server error" +msgstr "Interne serverfout" + +#~ msgid "Invalid point in GPX file" +#~ msgstr "Ongeldig punt in GPX-bestand" + +#~ msgid "GPX file has no points in track" +#~ msgstr "GPX-bestand mist punten in de track" diff --git a/server/locale/xget.sh b/server/locale/xget.sh new file mode 100755 index 0000000..06b5775 --- /dev/null +++ b/server/locale/xget.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +xgettext --keyword=translate:1,1t --keyword=translate:1c,2,2t \ + --keyword=translate:1,2,3t --keyword=translate:1c,2,3,4t \ + --keyword=gettext:1 --keyword=pgettext:1c,2 \ + --keyword=ngettext:1,2 --keyword=npgettext:1c,2,3 \ + --from-code=UTF-8 --language=C++ \ + ../src/*.cpp ../src/*.cppm \ + -o ../routemon.pot + +for po_file in *.po; do + msgmerge "$po_file" ../routemon.pot -U +done diff --git a/server/migrations/1_init_down.sql b/server/migrations/1_init_down.sql new file mode 100644 index 0000000..795e617 --- /dev/null +++ b/server/migrations/1_init_down.sql @@ -0,0 +1 @@ +DROP TABLE migration; diff --git a/server/migrations/1_init_up.sql b/server/migrations/1_init_up.sql new file mode 100644 index 0000000..7edb398 --- /dev/null +++ b/server/migrations/1_init_up.sql @@ -0,0 +1,7 @@ +BEGIN; + +CREATE TABLE migration (version); + +INSERT INTO migration VALUES (1); + +COMMIT; diff --git a/server/migrations/2_kaas_up.sql b/server/migrations/2_kaas_up.sql new file mode 100644 index 0000000..0c7837a --- /dev/null +++ b/server/migrations/2_kaas_up.sql @@ -0,0 +1,23 @@ +BEGIN; + +-- TODO enable strict mode so that primary key is automatically enforced to not be NULL +CREATE TABLE received_route ( + id INTEGER PRIMARY KEY, + + received_route_source route_source NOT NULL, + recieved_route_strava_id TEXT, + + -- ON DELETE what? + FOREIGN KEY (received_route_source_id) REFERENCES received_route_source (id), + CHECK (received_route_source IN ('gpx_upload', 'strava', 'rwgps')), + CHECK ((received_route_source = 'strava') = (received_route_strava_id IS NULL)) +); + +CREATE TABLE received_route_linestring ( + received_route_id INT NOT NULL, + linestring BLOB NOT NULL, + + FOREIGN KEY (received_route_id) REFERENCES received_route (id) +); + +COMMIT; diff --git a/server/routemon.pot b/server/routemon.pot new file mode 100644 index 0000000..711df4b --- /dev/null +++ b/server/routemon.pot @@ -0,0 +1,58 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-28 09:20+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../src/http_server.cppm:232 +msgid "No body expected for this request" +msgstr "" + +#: ../src/http_server.cppm:500 +msgid "Bad request" +msgstr "" + +#: ../src/http_server.cppm:511 ../src/http_server.cppm:562 +msgid "Method not allowed" +msgstr "" + +#: ../src/http_server.cppm:527 +msgid "" +"Path of normalized (RFC 3986, § 6) origin-form request-target (RFC 9112, § " +"3.2.1) should be absolute" +msgstr "" + +#: ../src/http_server.cppm:538 +msgid "Not found" +msgstr "" + +#: ../src/http_server.cppm:549 +msgid "Method not implemented" +msgstr "" + +#: ../src/http_server.cppm:573 +msgid "" +"Invalid request-target, expected asterisk-form or origin-form (see RFC 9112, " +"§ 3.2)" +msgstr "" + +#: ../src/srv.cppm:148 +msgid "Failed to parse GPX file" +msgstr "" + +#: ../src/srv.cppm:160 +msgid "Internal server error" +msgstr "" diff --git a/server/src/api.cpp b/server/src/api.cpp new file mode 100644 index 0000000..b3c8e31 --- /dev/null +++ b/server/src/api.cpp @@ -0,0 +1,206 @@ +module; + +#include +#include + +module routemon:api$impl; + +import std; +import :api; +import :datex2; +import :geo; +import :gpx; +import :log; +import :req_ctx; +import :time; +import :trace; + +namespace { + + namespace chrono = std::chrono; + namespace json = boost::json; + namespace views = std::views; + +} // namespace + +namespace routemon::api { + + auto json_value_from_point(geo::point const& p) -> json::value { + return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; + } + auto json_value_from_linestring(geo::linestring const& ls) -> json::value { + json::array a; + for (auto const& p : ls) + a.push_back(json_value_from_point(p)); + return a; + } + auto json_value_from_linestrings(std::vector const& lss) -> json::value { + json::array a; + for (auto const& ls : lss) + a.push_back(json_value_from_linestring(ls)); + return a; + } + + auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void { + jv = json::object{ + {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)}, + }; + } + auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void { + jv = json::object{ + {"id", json::value_from(sit.id)}, + {"location", sit.location ? json_value_from_point(*sit.location) : nullptr}, + {"comments", json::value_from(sit.comments)}, + {"relevant_road_closures", json::value_from(sit.relevant_road_closures)}, + }; + } + auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void { + jv = json::object{ + {"points", json_value_from_linestring(seg.points)}, + }; + } + auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void { + jv = json::object{ + {"segments", json::value_from(track.segments)}, + }; + } + auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void { + jv = json::object{ + {"tracks", json::value_from(res.tracks)}, + {"relevant_situations", json::value_from(res.relevant_situations)}, + }; + } + auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void { + jv = json::object{ + {"using_publication_of", std::format("{:%FT%TZ}", info.using_publication_of)}, + }; + } + + handler::handler(log::logger const& l, datex2::situation_publication pub) + : l_{l.sub("handler")}, pub_{std::move(pub)} + { + l_.info("Building indices"); + auto const before_build = chrono::steady_clock::now(); + for (auto const& sit : pub_.situations) { + for (auto const& rc : sit->road_closures) { + for (auto const& ls : rc->relevant_line_strings) { + auto box = geo::box{}; + bgeo::envelope(*ls, box); + lse_index_.insert(std::make_tuple(box, ls, rc)); + } + for (auto p : rc->relevant_points) { + p_index_.insert(std::make_pair(p, rc)); + } + } + } + auto const after_build = chrono::steady_clock::now(); + auto const dur_build = chrono::duration_cast(after_build - before_build); + l_.info("Indices built in {}", dur_build); + l_.info("LSE index size: {}", lse_index_.size()); + l_.info("Point index size: {}", p_index_.size()); + } + + auto handler::process_gpx(gpx::file&& gpx_file) -> std::optional { + auto const now = chrono::utc_clock::now(); + auto const relevant = std::initializer_list{time::period{now - chrono::days(7), now + chrono::days(7)}}; + auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; + + auto splits_with_overlap_segments = std::vector{}; + for (auto const& track : gpx_file.tracks) + for (auto const& seg : track.segments) + geo::split_linestring_with_overlap_segments(seg.waypoints, 5000 /* meters max total dist until a new split is forced */, + splits_with_overlap_segments); + auto const before_query = chrono::steady_clock::now(); + + l_.debug("Querying for relevant situations"); + auto relevant_road_closures = std::unordered_set>{}; + auto ls_checked = 0uz; + auto p_checked = 0uz; + auto i = 0; + for (geo::linestring const& part : splits_with_overlap_segments) { + l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); + + auto part_box = geo::box{}; + bgeo::envelope(part, part_box); + + for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); it != lse_index_.qend(); it++) { + // Cannot use structured bindings here, as boost::geometry::get interferes with ADL. + // It is a candidate as the namespace boost::geometry is part of the associated namespace set, + // which happens because geo::linestring ≡ boost::geometry::model::linestring is part + // of the whole tuple type (lse_index_value) that is the value_type of the iterator. + std::shared_ptr const& ls = std::get<1>(*it); + std::shared_ptr const& rc = std::get<2>(*it); + if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) + continue; + if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0) + relevant_road_closures.emplace(rc); + ls_checked++; + } + for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); it != p_index_.qend(); it++) { + // Cannot use structured bindings here for the same reason as above. + geo::point const& p = std::get<0>(*it); + std::shared_ptr const& rc = std::get<1>(*it); + if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) + continue; + if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0) + relevant_road_closures.emplace(rc); + p_checked++; + } + } + + auto const after_query = chrono::steady_clock::now(); + l_.debug("Done (checked {} line string(s) and {} point(s)) in {}", + ls_checked, p_checked, chrono::duration_cast(after_query - before_query)); + + auto relevant_situations = std::unordered_set>{}; + for (auto const& rc : relevant_road_closures) + relevant_situations.emplace(rc->parent); + + l_.debug("Identified {} relevant road closure(s), part of {} unique situation(s)", + relevant_road_closures.size(), relevant_situations.size()); + for (auto const& sit : relevant_situations) + l_.debug("Relevant situation: {}", sit->id); + + return process_gpx_result{ + .tracks = gpx_file.tracks + | views::transform([](auto const& trk) -> track { + return { + .segments = trk.segments + | views::transform([](auto const& seg) -> track_segment { + return {.points = seg.waypoints}; + }) + | std::ranges::to>(), + }; + }) + | std::ranges::to>(), + .relevant_situations = relevant_situations + | views::transform([&](std::shared_ptr sit) -> relevant_situation { + return { + .id = sit->id, + .location = sit->location, + .comments = sit->comments, + .relevant_road_closures = relevant_road_closures + | views::filter([&](std::shared_ptr const& rc) -> bool { + return std::shared_ptr{rc->parent} == sit; + }) + | views::transform([](std::shared_ptr const& rc) -> relevant_road_closure { + return { + .relevant_lss = rc->relevant_line_strings + | views::transform([](auto const& lsp) -> geo::linestring { + return *lsp; + }) + | std::ranges::to>(), + }; + }) + | std::ranges::to>(), + }; + }) + | std::ranges::to>(), + }; + } + + auto handler::sysinfo() -> struct sysinfo { + return {.using_publication_of = pub_.publication_time}; + } + +} // namespace routemon::api diff --git a/server/src/api.cppm b/server/src/api.cppm new file mode 100644 index 0000000..caeb44c --- /dev/null +++ b/server/src/api.cppm @@ -0,0 +1,79 @@ +module; + +#include +#include + +export module routemon:api; + +import std; +import :datex2; +import :geo; +import :gpx; +import :log; +import :time; +import :trace; + +namespace { + + namespace chrono = std::chrono; + namespace json = boost::json; + namespace views = std::views; + +} // namespace + +export +namespace routemon::api { + + struct relevant_road_closure { + std::vector relevant_lss; + }; + auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void; + + struct relevant_situation { + std::string id; + std::optional location; + std::vector comments; + std::vector relevant_road_closures; + }; + auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void; + + struct track_segment { + geo::linestring points; + }; + auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void; + + struct track { + std::vector segments; + }; + auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void; + + struct process_gpx_result { + std::vector tracks; + std::vector relevant_situations; + }; + auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void; + + struct sysinfo { + time::timestamp using_publication_of; + }; + auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void; + + class handler { + using lse_index_value = std::tuple, std::shared_ptr>; + using p_index_value = std::pair>; + using lse_index = bgeo::index::rtree>; + using p_index = bgeo::index::rtree>; + + log::logger l_; + datex2::situation_publication pub_; + lse_index lse_index_; + p_index p_index_; + + public: + explicit handler(log::logger const& l, datex2::situation_publication pub); + + auto process_gpx(gpx::file&& gpx_file) -> std::optional; + auto sysinfo() -> sysinfo; + }; + +} // namespace routemon::api diff --git a/server/src/config.cpp b/server/src/config.cpp new file mode 100644 index 0000000..3b17691 --- /dev/null +++ b/server/src/config.cpp @@ -0,0 +1,167 @@ +module; + +#include +#include + +module routemon:config$impl; + +import std; +import :config; +import :log; +import :util; + +namespace json = boost::json; + +namespace routemon::config { + + class location { + std::optional>> next_; + + auto append_to(std::string& s) const -> void { + if (next_) { + s += next_->first; + s += "."; + next_->second->append_to(s); + } + } + + public: + location() = default; + + explicit location(location const& next, std::string_view entry) + : next_{std::make_pair(entry, util::not_null{&next})} + {} + + [[nodiscard]] auto to_string() const -> std::string { + if (!next_) + return ""; + auto s = std::string{next_->first}; + next_->second->append_to(s); + return s; + } + + auto sub(std::string_view entry) const& -> location { + return location{*this, entry}; + } + }; + + class object_reader { + location loc_; + json::object const& obj_; + std::unordered_set visited_; + + public: + object_reader(json::value const& jv, location loc) + try : loc_{std::move(loc)}, obj_{jv.as_object()} + {} catch (boost::system::system_error const&) { + throw std::runtime_error{std::format("expected an object at {}", loc.to_string())}; + } + + auto check_unused() const -> void { + for (auto const& kv : obj_) { + if (!visited_.contains(std::string{kv.key()})) { + throw std::runtime_error{std::format("unexpected key {}", loc_.sub(kv.key()).to_string())}; + } + } + } + + template + auto expect_at(std::string_view key) -> T { + visited_.emplace(key); + auto const loc = loc_.sub(key); + if (auto const jv = obj_.try_at(key)) { + try { + return json::value_to(*jv, loc); + } catch (boost::system::system_error const& e) { + throw std::runtime_error{std::format("failed to read {}: {}", loc.to_string(), e.code().message())}; + } + } else { + throw std::runtime_error{std::format("did not find expected key {}", loc.to_string())}; + } + } + }; + + auto as_checked_object(json::value const& jv, location const& loc, std::invocable auto const& f) -> decltype(f(std::declval())) { + auto r = object_reader{jv, loc}; + auto&& v = f(r); + r.check_unused(); + return std::forward(v); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> rwgps { + return as_checked_object(jv, loc, [](object_reader& r) -> rwgps { + return { + .api_key = r.expect_at("api_key"), + .auth_token = r.expect_at("auth_token"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> situations { + return as_checked_object(jv, loc, [](object_reader& r) -> situations { + return { + .datex2_filename = r.expect_at("datex2_filename"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> database { + return as_checked_object(jv, loc, [](object_reader& r) -> database { + return { + .sqlite3_filename = r.expect_at("sqlite3_filename"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> http_server { + return as_checked_object(jv, loc, [](object_reader& r) -> http_server { + return { + .lax_cors = r.expect_at("lax_cors"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> logger { + return as_checked_object(jv, loc, [obj_loc = loc](object_reader& r) -> logger { + auto const level_str = r.expect_at("level"); + auto level = log::level{}; + if (level_str == "debug") + level = log::level::debug; + else if (level_str == "info") + level = log::level::info; + else if (level_str == "warn") + level = log::level::warn; + else if (level_str == "error") + level = log::level::error; + else + throw std::runtime_error{std::format("unable to parse log level {:?} at {}: expected one of {{debug, info, warn, error}}", level_str, obj_loc.sub("level").to_string())}; + return logger{level}; + }); + } + + auto json_value_to_app(json::value const& jv) -> app { + return as_checked_object(jv, location{}, [](object_reader& r) -> app { + return { + .rwgps = r.expect_at("rwgps"), + .situations = r.expect_at("situations"), + .database = r.expect_at("database"), + .http_server = r.expect_at("http_server"), + .logger = r.expect_at("logger"), + }; + }); + } + + auto load_file(std::string const& filename) -> app { + auto f = std::ifstream{filename}; // TODO: ensure that we are opening in binary mode? + if (!f.is_open()) + throw std::runtime_error{std::format("failed to open {}", filename)}; + auto jv = json::value{}; + try { + jv = json::parse(f); + } catch (boost::system::system_error const& e) { + throw std::runtime_error{std::format("failed to parse: {}", e.code().message())}; + } + return json_value_to_app(jv); + } + +} // namespace routemon::config diff --git a/server/src/config.cppm b/server/src/config.cppm new file mode 100644 index 0000000..ef827a6 --- /dev/null +++ b/server/src/config.cppm @@ -0,0 +1,39 @@ +export module routemon:config; + +import std; +import :log; + +namespace routemon::config { + + export struct rwgps { + std::string api_key; + std::string auth_token; + }; + + export struct situations { + std::string datex2_filename; + }; + + export struct database { + std::string sqlite3_filename; + }; + + export struct http_server { + bool lax_cors; + }; + + export struct logger { + log::level level; + }; + + export struct app { + rwgps rwgps; + situations situations; + database database; + http_server http_server; + logger logger; + }; + + export auto load_file(std::string const& filename) -> app; + +} // namespace routemon::config diff --git a/server/src/database.cppm b/server/src/database.cppm new file mode 100644 index 0000000..0312dda --- /dev/null +++ b/server/src/database.cppm @@ -0,0 +1,37 @@ +export module routemon:database; + +import std; +import :sqlite3; + +namespace routemon::database { + + static constexpr std::int64_t expected_database_version = 1; + + export class connection { + sqlite3::connection dbc_; + + explicit connection(sqlite3::connection dbc) : dbc_{std::move(dbc)} {} + + friend auto open(std::string const& filename) -> std::shared_ptr; + + public: + // Nothing here yet + }; + + export auto open(std::string const& filename) -> std::shared_ptr { + auto dbc = sqlite3::open(filename); + try { + auto version = std::optional{}; + dbc.query("SELECT version FROM migration;").scan_single(version); + if (!version) + throw std::runtime_error{"failed to fetch database migration version"}; + if (version != expected_database_version) { + throw std::runtime_error{std::format("database migration version ({}) does not match expected version ({}), consider running migrations", *version, expected_database_version)}; + } + } catch (std::exception const& e) { + throw std::runtime_error{std::format("failed to query database version: {}", e.what())}; + } + return std::shared_ptr{new connection{std::move(dbc)}}; + } + +} // namespace routemon::database diff --git a/server/src/datex2.cppm b/server/src/datex2.cppm new file mode 100644 index 0000000..b507e6f --- /dev/null +++ b/server/src/datex2.cppm @@ -0,0 +1,296 @@ +module; + +#include +#include +#include + +#include + +export module routemon:datex2; + +import std; +import :geo; +import :time; +import :util; + +using namespace std::literals::string_view_literals; + +namespace routemon::datex2 { + + export struct situation; + + export struct road_closure { + std::weak_ptr parent; + std::optional validity; + std::vector relevant_points = {}; + std::vector> relevant_line_strings = {}; + }; + + export struct situation { + std::string id; + std::optional location = std::nullopt; // as shown on the map, not used for querying + std::vector comments = {}; + std::vector> road_closures = {}; + }; + + export struct situation_publication { + time::timestamp publication_time; + std::vector> situations; + }; + + auto parse_timestamp(char const* in) -> std::optional { + auto res = time::timestamp{}; + auto is = std::istringstream{in}; + is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); + return is.fail() ? std::nullopt : std::make_optional(res); + } + + export class loader { + // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) + bgeo::srs::transformation, bgeo::srs::static_epsg<4326>> etrs89_to_wgs84_{}; + + std::multiset warnings_; + + auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) -> void { + auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; + if (loc_xml_type == "loc:ItineraryByIndexedLocations") { + for (auto const loc_cont_xml : loc_xml.children("loc:locationContainedInItinerary")) { + add_location_from_xml(rc, loc_cont_xml.child("loc:location")); + } + } else if (loc_xml_type == "loc:LinearLocation" || loc_xml_type == "loc:SingleRoadLinearLocation") { + auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); + if (!loc_gml_xml) + return; + + auto const srs_name = std::string_view{loc_gml_xml.attribute("srsName").value()}; + if (srs_name != "WGS 84"sv) { + warnings_.insert(std::format("don't now how to handle the CRS {}", srs_name)); + return; + } + auto const pos_list_str = std::string_view{loc_gml_xml.child_value("loc:posList")}; + // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn + + auto ls = std::make_shared(); + + auto lat_set = false; + auto lat = 0.0; + for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) { + auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str}); + if (!mlat_or_long) { + warnings_.insert(std::format("failed to parse coordinate {:?}", std::string_view{lat_or_long_str})); + return; + } + + if (!lat_set) { + lat = *mlat_or_long; + lat_set = true; + } else { + bgeo::append(*ls, geo::point{*mlat_or_long, lat}); + lat = 0; + lat_set = false; + } + } + + if (bgeo::is_empty(*ls)) { + warnings_.emplace("empty line string in data set"); + return; + } + + rc.relevant_line_strings.push_back(ls); + } else if (loc_xml_type == "loc:PointLocation") { + auto const& coords_xml = loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); + if (!coords_xml) + return; + + auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); + auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); + if (!mlat || !mlon) { + warnings_.emplace("failed to parse PointLocation coordinates"); + return; + } + + // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: + // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ + // maar heeft het UML-model van DATEX II v3 het over ETRS 89: + // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm + + auto const coords_etrs89 = geo::point{*mlon, *mlat}; + auto coords_wgs84 = geo::point{}; + etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); + + rc.relevant_points.push_back(coords_wgs84); + } else { + warnings_.insert(std::format("don't know how to hande location of type {}, ignoring", loc_xml.attribute("xsi:type").value())); + return; + } + } + + auto handle_road_or_carriageway_or_lane_management(pugi::xml_node const& record_xml, std::weak_ptr parent) -> std::optional> { + auto const type = std::string_view{record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value()}; + if (type != "carriagewayClosures" && type != "roadClosed") + // TODO: checken of er nog andere types fietsers de doorgang zouden kunnen blokkeren? + return std::nullopt; + + auto const& restricted_vehicle_types_xml = record_xml.child("sit:forVehiclesWithCharacteristicsOf"); + bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); + for (auto const vehicle_type_xml : restricted_vehicle_types_xml.children("com:vehicleType")) { + auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; + if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" || + vehicle_type == "unknown" || vehicle_type == "other") { + likely_restriction_for_bikes = true; + } + } + if (!likely_restriction_for_bikes) + return std::nullopt; + + //---- Check if within the defined validity period + + auto validity = std::optional{}; + auto const& validity_xml = record_xml.child("sit:validity"); + if (validity_xml && validity_xml.child_value("com:validityStatus") == "definedByValidityTimeSpec"sv) { + auto const& validity_spec_xml = validity_xml.child("com:validityTimeSpecification"); + + auto valid_periods = std::vector{}; + auto exception_periods = std::vector{}; + + // TODO: com:overallEndTime may be missing (according to the DATEX II v3 data model) + auto const overall_start_time = parse_timestamp(validity_spec_xml.child_value("com:overallStartTime")); + auto const overall_end_time = parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); + if (overall_start_time && overall_end_time && *overall_start_time < *overall_end_time) { + valid_periods.emplace_back(*overall_start_time, *overall_end_time); + + for (auto const valid_period_xml : validity_xml.children("com:validPeriod")) { + auto const start_of_period = parse_timestamp(valid_period_xml.child_value("com:startOfPeriod")); + auto const end_of_period = parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); + if (start_of_period && end_of_period && *start_of_period < *end_of_period) { + valid_periods.emplace_back(*start_of_period, *end_of_period); + } + } + for (auto const exception_period_xml : validity_xml.children("com:exceptionPeriod")) { + auto const start_of_period = parse_timestamp(exception_period_xml.child_value("com:startOfPeriod")); + auto const end_of_period = parse_timestamp(exception_period_xml.child_value("com:endOfPeriod")); + if (start_of_period && end_of_period && *start_of_period < *end_of_period) { + exception_periods.emplace_back(*start_of_period, *end_of_period); + } + } + + validity = time::period_seq{valid_periods.begin(), valid_periods.end()} + .except(time::period_seq{exception_periods.begin(), exception_periods.end()}); + } else { + warnings_.insert(std::format("invalid overall start / end time (start time: {}, end time: {})", + validity_spec_xml.child_value("com:overallStartTime"), + validity_spec_xml.child_value("com:overallEndTime"))); + return std::nullopt; + } + } + + //---- Try to extract the location info + + auto rc = std::make_shared(std::move(parent), validity); + add_location_from_xml(*rc, record_xml.child("sit:locationReference")); + return rc; + } + + public: + [[nodiscard]] auto load_situation_publication(std::string const& filename) -> situation_publication { + auto doc = pugi::xml_document{}; + if (auto result = doc.load_file(filename.c_str()); !result) { + throw std::runtime_error{result.description()}; + } + auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); + auto mpublication_time = parse_timestamp(payload_xml.child_value("com:publicationTime")); + if (!mpublication_time) + throw std::runtime_error{"provided publication does not name publication time"}; + + auto situations = std::vector>{}; + for (auto const sit_xml : payload_xml.children("sit:situation")) { + auto id = std::string_view{sit_xml.attribute("id").value()}; + + auto const sit = std::make_shared(std::string{id}); + situations.push_back(sit); + + auto const& header_info_xml = sit_xml.child("sit:headerInformation"); + if (header_info_xml.child_value("com:informationStatus") != "real"sv) + continue; + + for (auto const record_xml : sit_xml.children("sit:situationRecord")) { + auto const record_type = std::string_view{record_xml.attribute("xsi:type").value()}; + auto const primary_record_types = std::unordered_set{ + "sit:Roadworks", + /* { */ "sit:MaintenanceWorks", + /* | */ "sit:ConstructionWorks", + /* } */ + "sit:Obstruction", + /* { */ "sit:EnvironmentalObstruction", + /* | */ "sit:GeneralObstruction", + /* | */ "sit:InfrastructureDamageObstruction", + /* } */ + "sit:Activity", + /* { */ "sit:PublicEvent", + /* } */ + }; + + if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") { + if (auto rc = handle_road_or_carriageway_or_lane_management(record_xml, sit)) { + sit->road_closures.push_back(*rc); + } + } else if (primary_record_types.contains(record_type)) { + for (auto const comment_xml : record_xml.children("sit:generalPublicComment")) { +// if (comment_xml.child_value("sit:commentType") == "internalNote"sv) { + auto candidate = std::optional>{}; // (text, language) + for (auto const comment_value_xml : comment_xml.child("sit:comment").child("com:values").children("com:value")) { + if (!candidate || + comment_value_xml.attribute("lang").value() == "nl"sv || + (candidate->second != "nl"sv && comment_value_xml.attribute("lang").value() == "nl"sv)) { + candidate = std::make_pair(comment_value_xml.child_value(), comment_value_xml.attribute("lang").value()); + } + } + if (candidate) { + auto already_present = false; + for (auto const& comment : sit->comments) + already_present = already_present || comment == candidate->first; + if (!already_present) { + sit->comments.emplace_back(candidate->first); + } + } +// } + } + + if (auto const location_ref_xml = record_xml.child("sit:locationReference")) { + if (location_ref_xml.attribute("xsi:type").value() == "loc:PointLocation"sv) { + if (auto const coords_xml = location_ref_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates")) { + auto const mlat = util::parse_double(coords_xml.child_value("loc:latitude")); + auto const mlon = util::parse_double(coords_xml.child_value("loc:longitude")); + if (mlat && mlon) { + // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: + // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ + // maar heeft het UML-model van DATEX II v3 het over ETRS 89: + // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm + + auto const coords_etrs89 = geo::point{*mlon, *mlat}; + auto coords_wgs84 = geo::point{}; + etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); + + if (!sit->location) { + sit->location = coords_wgs84; + } + } + } + } + } + } + } + } + + return { + .publication_time = *mpublication_time, + .situations = situations, + }; + } + + [[nodiscard]] auto warnings() const -> std::multiset const& { + return warnings_; + } + }; + +} // namespace routemon::datex2 diff --git a/server/src/geo.cppm b/server/src/geo.cppm new file mode 100644 index 0000000..5cdbdd9 --- /dev/null +++ b/server/src/geo.cppm @@ -0,0 +1,40 @@ +module; + +#include + +export module routemon:geo; + +export namespace bgeo = boost::geometry; + +export namespace routemon::geo { + + using point = bgeo::model::point>; + using linestring = bgeo::model::linestring; + using box = bgeo::model::box; + using stype = bgeo::srs::spheroid; + using vincenty_strategy = bgeo::strategy::distance::vincenty; + + auto split_linestring_with_overlap_segments(linestring const& ls, double max_split_distance_m, std::vector& append_to) -> void { + if (bgeo::is_empty(ls)) + return; + + auto current_ls = linestring{}; + auto current_ls_length = 0.0; + auto previous = std::optional{}; + bgeo::for_each_point(ls, [&](point p) -> void { + bgeo::append(current_ls, p); + if (previous) { + auto d = bgeo::distance(*previous, p, vincenty_strategy()); + current_ls_length += d; + if (current_ls_length > max_split_distance_m) { + append_to.push_back(std::move(current_ls)); + current_ls = linestring{*previous, p}; + current_ls_length = d; + } + } + previous = p; + }); + append_to.emplace_back(std::move(current_ls)); + } + +} // namespace routemon::geo diff --git a/server/src/gpx.cpp b/server/src/gpx.cpp new file mode 100644 index 0000000..62dccff --- /dev/null +++ b/server/src/gpx.cpp @@ -0,0 +1,189 @@ +module; + +#include + +module routemon:gpx$impl; + +import std; +import :gpx; +import :util; +import :xml; + +using namespace std::literals::string_view_literals; + +namespace routemon::gpx { + + namespace v10 { + + constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv; + auto qname(std::string_view local) -> xml::qname_view { + return {.ns_uri = xmlns, .local = local}; + } + + auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { + auto parse_xml_double = [](std::string_view sv) -> std::optional { + return util::parse_double(sv, std::chars_format::fixed); + }; + auto mlat = std::optional{}; + auto mlon = std::optional{}; + for (auto const& [name, value] : attrs) { + if (name == qname("lat")) { + mlat = parse_xml_double(value); + } else if (name == qname("lon")) { + mlon = parse_xml_double(value); + } + } + if (!mlat || !mlon) + throw std::runtime_error{"expected valid latitude and longitude for waypoint"}; + co_await xml::ignore_contents(e); + co_return geo::point{*mlon, *mlat}; + } + + auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser { + auto s = track_segment{}; + while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo())) + bgeo::append(s.waypoints, *mwpt); + co_return std::move(s); + } + + auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser { + auto t = track{}; + t.name = co_await allow_element(e, qname("name"), xml::hohalo()); + co_await allow_element(e, qname("cmt"), xml::hohalo()); + t.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); + co_await xml::ignore_contents(e, /* until */ qname("trkseg")); + while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo())) + t.segments.push_back(std::move(*mseg)); + co_return std::move(t); + } + + auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { + auto f = file{}; + if (attrs.lookup(qname("version")) != "1.0"sv) + throw std::runtime_error{"expected GPX version to be 1.0"}; + if (auto mcreator = attrs.lookup(qname("creator"))) + f.creator = *mcreator; + else + throw std::runtime_error{"expected GPX file to have creator"}; + + f.meta.name = co_await allow_element(e, qname("name"), xml::hohalo()); + f.meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); + + co_await xml::ignore_contents(e, /* until */ qname("trk")); + while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo())) + f.tracks.push_back(std::move(*mtrk)); + co_await xml::ignore_contents(e); + + co_return std::move(f); + } + + } // namespace v10 + + namespace v11 { + + constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv; + auto qname(std::string_view local) -> xml::qname_view { + return {.ns_uri = xmlns, .local = local}; + } + + auto parse_metadata(xml::executor_ref e, xml::attribute_view) -> xml::parser { + auto meta = metadata{}; + meta.name = co_await allow_element(e, qname("name"), xml::hohalo()); + meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); + co_await xml::ignore_contents(e); + co_return meta; + } + + auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { + auto parse_xml_double = [](std::string_view sv) -> std::optional { + return util::parse_double(sv, std::chars_format::fixed); + }; + auto mlat = std::optional{}; + auto mlon = std::optional{}; + for (auto const& [name, value] : attrs) { + if (name == qname("lat")) { + mlat = parse_xml_double(value); + } else if (name == qname("lon")) { + mlon = parse_xml_double(value); + } + } + if (!mlat || !mlon) + throw std::runtime_error{"expected valid latitude and longitude for waypoint"}; + co_await xml::ignore_contents(e); + co_return geo::point{*mlon, *mlat}; + } + + auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser { + auto s = track_segment{}; + while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo())) + bgeo::append(s.waypoints, *mwpt); + co_await allow_element(e, qname("extensions"), xml::hohalo()); + co_return std::move(s); + } + + auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser { + auto t = track{}; + t.name = co_await allow_element(e, qname("name"), xml::hohalo()); + co_await allow_element(e, qname("cmt"), xml::hohalo()); + t.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); + co_await xml::ignore_contents(e, /* until */ qname("trkseg")); + while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo())) + t.segments.push_back(std::move(*mseg)); + co_return std::move(t); + } + + auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { + auto f = file{}; + if (attrs.lookup(qname("version")) != "1.1"sv) + throw std::runtime_error{"expected GPX version to be 1.1"}; + if (auto mcreator = attrs.lookup(qname("creator"))) + f.creator = *mcreator; + else + throw std::runtime_error{"expected GPX file to have creator"}; + + if (auto mmeta = co_await allow_element(e, qname("metadata"), xml::hohalo())) + f.meta = *mmeta; + co_await xml::ignore_contents(e, /* until */ qname("trk")); + while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo())) + f.tracks.push_back(std::move(*mtrk)); + co_await allow_element(e, qname("extensions"), xml::hohalo()); + + co_return std::move(f); + } + + } // namespace v11 + + auto parse_file(xml::executor_ref e) -> xml::parser { + auto decl = co_await expect_event(e); + if (decl.version != "1.0"sv) + throw std::runtime_error{std::format("unsupported XML version, got {}", std::string_view{decl.version})}; + if (decl.encoding != "UTF-8"sv) + throw std::runtime_error{"unsupported encoding"}; + if (auto mf = co_await allow_element(e, v10::qname("gpx"), xml::hohalo())) + co_return std::move(*mf); + if (auto mf = co_await allow_element(e, v11::qname("gpx"), xml::hohalo())) + co_return std::move(*mf); + throw std::runtime_error{"no supported GPX document found"}; + } + + reader::reader() + : p_{parse_file(util::not_null{&e_})} + { e_.set_continuation(p_.promise().base_handle()); } + + auto reader::init() -> void { + e_.start(); + } + + auto reader::put(std::string_view buf) -> void { + e_.read(buf, false); + } + + auto reader::finish() -> gpx::file { + e_.read(std::string_view{}, true); + e_.end(); + // Promise is still alive since the last coroutine performs a + // symmetric transfer to std::noop_coroutine() in final_suspend(). + return std::move(p_.promise().returned_value()); + } + +} // namespace routemon::gpx diff --git a/server/src/gpx.cppm b/server/src/gpx.cppm new file mode 100644 index 0000000..20e6242 --- /dev/null +++ b/server/src/gpx.cppm @@ -0,0 +1,43 @@ +export module routemon:gpx; + +import std; +import :geo; +import :util; +import :xml; + +namespace routemon::gpx { + + struct metadata { + std::optional name; + std::optional desc; + }; + + struct track_segment { + geo::linestring waypoints; + }; + + struct track { + std::optional name; + std::optional desc; + std::vector segments; + }; + + struct file { + std::string creator; + metadata meta; + std::vector tracks; + }; + + class reader { + xml::executor e_; + xml::parser p_; + + public: + explicit reader(); + + auto init() -> void; + auto put(std::string_view buf) -> void; + [[nodiscard]] auto finish() -> gpx::file; + }; + +} // namespace routemon::gpx diff --git a/server/src/http_client.cppm b/server/src/http_client.cppm new file mode 100644 index 0000000..d5316d6 --- /dev/null +++ b/server/src/http_client.cppm @@ -0,0 +1,62 @@ +module; + +#include +#include +#include +#include +#include + +export module routemon:http.client; + +export import :http.common; + +namespace net = boost::asio; +namespace ssl = net::ssl; +using tcp = net::ip::tcp; + +namespace routemon::http { + + export class client { + net::io_context& ioc_; + ssl::context sslc_{ssl::context::tlsv12_client}; + tcp::resolver resolver_; + + public: + explicit client(net::io_context& ioc) + : ioc_{ioc}, resolver_{ioc} + { + sslc_.set_default_verify_paths(); + sslc_.set_verify_mode(net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); + } + + template + auto do_request(bhttp::request& req) -> bhttp::response { + auto stream = ssl::stream{ioc_, sslc_}; + + auto host = std::string{req.at(bhttp::field::host)}; + if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { + throw beast::system_error(static_cast(::ERR_get_error()), + net::error::get_ssl_category()); + } + stream.set_verify_callback(ssl::host_name_verification(host)); + auto const results = resolver_.resolve(host, "443"); + beast::get_lowest_layer(stream).connect(results); + stream.handshake(ssl::stream_base::client); + + req.set(bhttp::field::user_agent, "routemon/1.0"); + bhttp::write(stream, req); + + auto buffer = beast::flat_buffer{}; + auto res = bhttp::response{}; + bhttp::read(stream, buffer, res); + + auto ec = beast::error_code{}; + stream.shutdown(ec); + if (ec != net::ssl::error::stream_truncated) + throw beast::system_error{ec}; + + return res; + } + }; + +} // namespace routemon::http diff --git a/server/src/http_common.cppm b/server/src/http_common.cppm new file mode 100644 index 0000000..3c38da7 --- /dev/null +++ b/server/src/http_common.cppm @@ -0,0 +1,139 @@ +module; + +#include +#include + +export module routemon:http.common; + +namespace beast = boost::beast; +export namespace bhttp = beast::http; + +namespace boost::beast { + + namespace concepts { + + template + concept buffers_generator = beast::is_buffers_generator::value; + + template + concept const_buffer_sequence = beast::is_const_buffer_sequence::value; + + } // namespace concepts + + namespace http::concepts { + + template + concept fields = is_fields::value; + + template + concept body = is_body::value; + + template + concept body_reader = is_body_reader::value; + + } // namespace http::concepts + +} // namespace boost::beast + +namespace routemon::http { + + struct supported_verb { + enum supported_verb_t : std::uint8_t { + options, + delete_, + get, + head, + post, + put, + }; + + supported_verb_t value; + + supported_verb(supported_verb_t value) : value{value} {} + + static auto from(bhttp::verb v) -> std::optional { + switch (v) { + case bhttp::verb::options: return supported_verb::options; + case bhttp::verb::delete_: return supported_verb::delete_; + case bhttp::verb::get: return supported_verb::get; + case bhttp::verb::head: return supported_verb::head; + case bhttp::verb::post: return supported_verb::post; + case bhttp::verb::put: return supported_verb::put; + default: return std::nullopt; + } + } + + operator bhttp::verb() const { + switch (value) { + case supported_verb::options: return bhttp::verb::options; + case supported_verb::delete_: return bhttp::verb::delete_; + case supported_verb::get: return bhttp::verb::get; + case supported_verb::head: return bhttp::verb::head; + case supported_verb::post: return bhttp::verb::post; + case supported_verb::put: return bhttp::verb::put; + } + } + }; + + export struct verb_set { + bool delete_ : 1 = false; + bool get : 1 = false; + bool head : 1 = false; + bool post : 1 = false; + bool put : 1 = false; + bool options : 1 = false; + + auto enable(supported_verb v) -> void { + switch (v.value) { + case supported_verb::delete_: + delete_ = true; + break; + case supported_verb::get: + get = true; + break; + case supported_verb::head: + head = true; + break; + case supported_verb::post: + post = true; + break; + case supported_verb::put: + put = true; + break; + case supported_verb::options: + options = true; + break; + default:; + } + } + + auto operator==(verb_set const& rhs) const noexcept -> bool = default; + + auto empty() const -> bool { + return *this == verb_set{}; + } + + verb_set(std::initializer_list vs) { + for (auto const v : vs) enable(v); + } + + auto to_string() const -> std::string { + std::ostringstream ss; + bool wrote = false; + auto write = [&](bhttp::verb v) { + if (wrote) + ss << ", "; + ss << v; + wrote = true; + }; + if (delete_) write(bhttp::verb::delete_); + if (get) write(bhttp::verb::get); + if (head) write(bhttp::verb::head); + if (post) write(bhttp::verb::post); + if (put) write(bhttp::verb::put); + if (options) write(bhttp::verb::options); + return ss.str(); + } + }; + +} // namespace routemon::http diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm new file mode 100644 index 0000000..0770d97 --- /dev/null +++ b/server/src/http_server.cppm @@ -0,0 +1,661 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +export module routemon:http.server; + +import std; +import :config; +import :trace; +export import :http.common; +import :problem; + +namespace net = boost::asio; +using tcp = net::ip::tcp; + +namespace routemon::http { + + struct readable_request { + util::not_null*> p; + util::not_null strm; + util::not_null buf; + }; + + class presponse { + public: + using const_buffers_type = beast::span; + + private: + struct impl_base { + virtual ~impl_base() = default; + virtual auto header() -> bhttp::response_header& = 0; + virtual auto header() const -> bhttp::response_header const& = 0; + virtual auto is_done() const -> bool = 0; + virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; + virtual auto consume(std::size_t n) -> void = 0; + virtual auto keep_alive() const -> bool = 0; + }; + std::unique_ptr impl_; + + template + class impl : public impl_base { + // Initializes in the response state. + // At the first call to prepare, we switch to the message generator state. + // After that point, header may not be called anymore (it will throw). + std::variant, bhttp::message_generator> state_; + + auto ensure_message_generator() -> bhttp::message_generator& { + if (auto prsp = std::get_if>(&state_)) { + auto rsp = bhttp::response{std::move(*prsp)}; + state_.template emplace(std::move(rsp)); + } + return std::get(state_); + } + + public: + explicit impl(bhttp::response&& rsp) : state_{std::move(rsp)} {} + + auto header() -> bhttp::response_header& override { + if (auto prsp = std::get_if>(&state_)) { + return prsp->base(); + } else { + // TODO: define custom exception type presponse::bad_header_access + throw std::logic_error{"header() may not be called after prepare()"}; + } + } + auto header() const -> bhttp::response_header const& override { + if (auto prsp = std::get_if>(&state_)) { + return prsp->base(); + } else { + throw std::logic_error{"header() may not be called after prepare()"}; + } + } + + auto is_done() const -> bool override { + if (auto pgen = std::get_if(&state_)) { + return pgen->is_done(); + } else /* still in the response state */ { + return false; + } + } + + auto prepare(beast::error_code& ec) -> const_buffers_type override { + return ensure_message_generator().prepare(ec); + } + + auto consume(std::size_t n) -> void override { + ensure_message_generator().consume(n); + } + + auto keep_alive() const noexcept -> bool override { + return state_.visit(util::overloaded{ + [](bhttp::response const& rsp) -> bool { + return rsp.keep_alive(); + }, + [](bhttp::message_generator const& gen) -> bool { + return gen.keep_alive(); + }, + }); + } + }; + + public: + template + explicit presponse(bhttp::response&& rsp) + : impl_{new impl{std::move(rsp)}} + {} + + auto header() -> bhttp::response_header& { + return impl_->header(); + } + auto header() const -> bhttp::response_header const& { + return impl_->header(); + } + + auto is_done() const -> bool { + return impl_->is_done(); + } + + auto prepare(beast::error_code& ec) -> const_buffers_type { + return impl_->prepare(ec); + } + + auto consume(std::size_t n) -> void { + return impl_->consume(n); + } + + auto keep_alive() const noexcept -> bool { + return impl_->keep_alive(); + } + }; + static_assert(beast::concepts::buffers_generator); + + template + using next_handler_t = std::function net::awaitable>; + + template + using middleware_t = std::function&, next_handler_t) -> net::awaitable>; + + template + auto lax_cors_middleware(Ctx ctx, bhttp::request_header& req_hdr, next_handler_t next) -> net::awaitable { + std::ignore = req_hdr; + auto prersp = co_await next(ctx); + prersp.header().set(bhttp::field::access_control_allow_origin, "*"); + co_return std::move(prersp); + } + + template + struct trace_id_ctx : InnerCtx { + trace::id trace_id = {}; + }; + + template + auto trace_id_middleware(OuterCtx ctx0, bhttp::request_header& req_hdr, next_handler_t> next) -> net::awaitable { + std::ignore = req_hdr; + auto ctx = trace_id_ctx{std::move(ctx0)}; + auto prersp = co_await next(std::move(ctx)); + prersp.header().set("X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()}); + prersp.header().insert(bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); + co_return std::move(prersp); + } + + struct base_ctx { + std::locale locale; + }; + + template + using basic_route_handler_fn_t = std::function const& matches) -> net::awaitable>; + + struct keep_alive { + bool value; + + explicit keep_alive(bool value) : value{value} {} + }; + + template + auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response { + auto rsp = bhttp::response{}; // HTTP version gets set later + rsp.result(status); + rsp.keep_alive(ka.value); + return rsp; + } + + auto problem_rsp(base_ctx const& ctx, problem::details const& problem, keep_alive ka) -> presponse { + auto rsp = make_rsp(problem.status, ka); + rsp.set(bhttp::field::content_type, "application/problem+json"); + rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); + rsp.prepare_payload(); + return presponse{std::move(rsp)}; + } + + struct preflight_response { + verb_set allow_methods; + std::vector allow_headers; + }; + auto make_preflight_rsp(preflight_response res, keep_alive ka) -> bhttp::response { + auto rsp = make_rsp(bhttp::status::no_content, ka); + auto allow_headers_str = res.allow_headers + | std::views::transform([](auto const& field) -> std::string_view { return bhttp::to_string(field); }) + | std::views::join_with(std::string_view{", "}) + | std::ranges::to(); + rsp.set(bhttp::field::access_control_allow_methods, res.allow_methods.to_string()); + rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); + rsp.prepare_payload(); + return rsp; + } + + // Using base_ctx instead of a template here since that saves you + // typing on invocation (and we do not care about the context type + // anyway, but all context types should derive from base_ctx). + template + auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable, presponse>> { + std::ignore = ctx; + auto p = bhttp::request_parser{std::move(*r.p)}; + co_await bhttp::async_read(*r.strm, *r.buf, p); + co_return std::move(p.release()); + } + + template<> + auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable, presponse>> { + auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); + if (ec == bhttp::error::unexpected_body) { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("No body expected for this request"), + .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body", + }; + co_return std::unexpected{problem_rsp(ctx, tpl.instantiate(), keep_alive{false})}; + } else if (ec) { + throw boost::system::system_error{ec}; + } + co_return r.p->release(); + } + + template + struct routed_ctx : InnerCtx { + verb_set route_methods; + }; + + template + requires requires(Ctx ctx) { + // Ctx must be derived from an instantiation of routed_ctx + [](routed_ctx const&) {}(ctx); + } + auto default_options_handler(Ctx const& ctx, readable_request r, std::vector const&) -> net::awaitable { + auto mreq = co_await read_request(ctx, std::move(r)); + if (!mreq) + co_return std::move(mreq.error()); + + if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) { + // CORS preflight request + co_return make_preflight_rsp(preflight_response{ + // TODO: should access-control-allow-methods contain OPTIONS? + .allow_methods = ctx.route_methods, + .allow_headers = {bhttp::field::content_type}, + }, keep_alive{mreq->keep_alive()}); + } else { + // Normal OPTIONS request + auto rsp = make_rsp(bhttp::status::no_content, keep_alive{mreq->keep_alive()}); + rsp.set(bhttp::field::allow, ctx.route_methods.to_string()); + rsp.prepare_payload(); + co_return std::move(rsp); + } + } + + auto global_options_handler(base_ctx const& ctx, readable_request r) -> net::awaitable { + // TODO: switch to "small (4KB) discarded" body type, similar to what Go does? + // Same goes for default_options_handler? Not sure. + if (auto res = co_await read_request(ctx, std::move(r)); !res) + co_return std::move(res.error()); + auto req = r.p->release(); + auto rsp = make_rsp(bhttp::status::no_content, keep_alive{req.keep_alive()}); + rsp.prepare_payload(); + co_return std::move(rsp); + } + + template + auto id_middleware(Ctx ctx, bhttp::request_header&, next_handler_t next) -> net::awaitable { + co_return co_await next(std::move(ctx)); + } + + template + auto middleware_compose(middleware_t ab, middleware_t bc) -> middleware_t { + return [ab = std::move(ab), bc = std::move(bc)](A a, bhttp::request_header& header, next_handler_t next) -> net::awaitable { + co_return co_await ab(std::move(a), header, [&](B b) -> net::awaitable { + co_return co_await bc(std::move(b), header, next); + }); + }; + } + + template + auto middleware_wrap_fn(middleware_t ab, basic_route_handler_fn_t fn) -> basic_route_handler_fn_t { + return [ab = std::move(ab), fn = std::move(fn)](A a_ctx, readable_request r, std::vector const& matches) -> net::awaitable { + co_return co_await ab(std::move(a_ctx), r.p->get().base(), [&](B b_ctx) -> net::awaitable { + co_return co_await fn(std::move(b_ctx), r, matches); + }); + }; + } + + template + requires requires(V v) { + { static_cast(v) }; + } + struct handler_map { + V options = {}; + V delete_ = {}; + V get = {}; + V head = {}; + V post = {}; + V put = {}; + + template + auto lookup(this Self&& self, supported_verb v) -> auto&& { + switch (v.value) { + case supported_verb::options: return std::forward(self).options; + case supported_verb::delete_: return std::forward(self).delete_; + case supported_verb::get: return std::forward(self).get; + case supported_verb::head: return std::forward(self).head; + case supported_verb::post: return std::forward(self).post; + case supported_verb::put: return std::forward(self).put; + } + } + + auto verbs() const -> verb_set { + auto set = verb_set{}; + if (static_cast(options)) + set.enable(supported_verb::options); + if (static_cast(delete_)) + set.enable(supported_verb::delete_); + if (static_cast(get)) + set.enable(supported_verb::get); + if (static_cast(head)) + set.enable(supported_verb::head); + if (static_cast(post)) + set.enable(supported_verb::post); + if (static_cast(put)) + set.enable(supported_verb::put); + return set; + } + + auto empty() const -> bool { + return verbs().empty(); + } + + template + auto map(std::invocable auto f) const -> handler_map + requires std::assignable_from> + { + return { + .options = static_cast(options) ? f(options) : U{}, + .delete_ = static_cast(delete_) ? f(delete_) : U{}, + .get = static_cast(get) ? f(get) : U{}, + .head = static_cast(head) ? f(head) : U{}, + .post = static_cast(post) ? f(post) : U{}, + .put = static_cast(put) ? f(put) : U{}, + }; + } + }; + + template + struct route_tree { + using leaves = handler_map>; + using named_subtrees = std::unordered_map; + using wildcard_subtree = std::indirect; + + leaves here; + // TODO: consider making the first alternative a radix tree + // Note: the map is the first variant here; the variant will be + // default-constructed with the default-constructed first + // alternative. The empty map denotes a lack of subtrees. + std::variant sub; + }; + + template + auto middleware_wrap_tree(middleware_t mw, route_tree const& tree) -> route_tree { + auto new_leaves = tree.here.template map>(std::bind_front(middleware_wrap_fn, mw)); + auto new_sub = tree.sub.visit(util::overloaded{ + [&mw](route_tree::named_subtrees const& subtrees) -> decltype(route_tree::sub) { + auto new_subtrees = typename route_tree::named_subtrees{}; + for (auto [seg, subtree] : subtrees) + new_subtrees[seg] = middleware_wrap_tree(mw, subtree); + return new_subtrees; + }, + [&mw](route_tree::wildcard_subtree const& subtree) -> decltype(route_tree::sub) { + return typename route_tree::wildcard_subtree{middleware_wrap_tree(mw, *subtree)}; + }, + }); + return {.here = new_leaves, .sub = new_sub}; + } + + template + concept match_arg = std::constructible_from; + + template + using route_handler_fn_t = std::function net::awaitable>; + + template + auto degen_route_handler(route_handler_fn_t fn) -> basic_route_handler_fn_t { + return [fn = std::move(fn)](Ctx ctx, readable_request r, std::vector const& matches) -> net::awaitable { + if (sizeof...(MatchArgs) != matches.size()) + throw std::runtime_error{"got unexpected amount of matches"}; + auto it = matches.begin(); + co_return co_await fn(std::move(ctx), r, MatchArgs{static_cast(*it++)}...); + }; + } + + template + struct ctree : route_tree { + template + auto wrap(middleware_t mw) const -> ctree { + return {middleware_wrap_tree(std::move(mw), *this)}; + } + }; + + template + struct dtree : handler_map> { + [[nodiscard]] auto to_leaves() const -> typename route_tree::leaves { + auto here = this->template map>(degen_route_handler); + if (!here.verbs().empty() && !static_cast(this->options)) + here.options = default_options_handler; + return here; + } + + [[nodiscard]] auto named_subtrees(std::initializer_list>> subtrees) const -> ctree { + auto sub = typename route_tree::named_subtrees{ + std::from_range, + subtrees | std::views::transform([](auto const& p) { + return std::make_pair(p.first, static_cast>(p.second)); + }) + }; + return {route_tree{.here = to_leaves(), .sub = sub}}; + } + + template + [[nodiscard]] auto wildcard_subtree(ctree subtree) -> ctree { + return {route_tree{.here = to_leaves(), .sub = typename route_tree::wildcard_subtree{static_cast>(subtree)}}}; + } + + [[nodiscard]] auto no_subtrees() const -> ctree { + return {route_tree{.here = to_leaves(), .sub = {}}}; + } + }; + + template PreRouteCtx> + class server { + log::logger l_; + locale::selector lsel_; + middleware_t global_middleware_; + route_tree> routes_; + + public: + explicit server(log::logger const& l, locale::selector&& lsel, middleware_t global_middleware, route_tree> routes) + : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, global_middleware_{std::move(global_middleware)}, routes_{std::move(routes)} + {} + + struct match_result { + util::not_null>> const*> route_handlers; + std::vector wildcard_matches; + + auto allowed_methods() const -> verb_set { + return route_handlers->verbs(); + } + }; + + auto match(boost::urls::segments_view segments) const -> std::optional { + auto const* tree = &routes_; + auto wildcard_matches = std::vector{}; + for (auto const& seg : segments) { + tree->sub.visit(util::overloaded{ + [&](route_tree>::named_subtrees const& subtrees) { + auto it = subtrees.find(seg); + tree = it == subtrees.end() ? nullptr : &it->second; + }, + [&](route_tree>::wildcard_subtree const& wildcard_subtree) { + wildcard_matches.push_back(seg); + tree = &*wildcard_subtree; + }, + }); + if (!tree) return std::nullopt; + } + if (tree->here.empty()) return std::nullopt; + return match_result{ + .route_handlers = util::not_null{&tree->here}, + .wildcard_matches = wildcard_matches, + }; + } + + auto route_request(PreRouteCtx ctx, readable_request r) const -> net::awaitable { + auto req_base = r.p->get().base(); + + auto const bad_request_tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Bad request"), + .type_uri = "https://routemon.fautchen.eu/problems/bad-request", + }; + + if (req_base.target() == "*") { + // request-target is in asterisk-form (RFC 9112, § 3.2.4), + // so the request must be a server-wide OPTIONS request. + + if (req_base.method() != bhttp::verb::options) { + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } + + co_return co_await global_options_handler(ctx, r); + } else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) { + // request-target is in origin-form (RFC 9112, § 3.2.1), + // so it must be a normal request (not a CONNECT or + // server-wide OPTIONS request). + + auto req_url = boost::urls::url{*mreq_url0}; + req_url.normalize(); + if (!req_url.is_path_absolute()) { + auto problem = bad_request_tpl.instantiate(). + set_detail(translate("Path of normalized (RFC 3986, § 6) " + "origin-form request-target (RFC " + "9112, § 3.2.1) should be " + "absolute")); + co_return problem_rsp(ctx, problem, keep_alive{false}); + } + + auto mres = match(req_url.segments()); + if (!mres) { + auto tpl = problem::tpl{ + .status = bhttp::status::not_found, + .title = translate("Not found"), + .type_uri = "https://routemon.fautchen.eu/problems/not-found", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } + + auto mverb = supported_verb::from(req_base.method()); + if (!mverb) { + // Method not implemented. + auto tpl = problem::tpl{ + .status = bhttp::status::not_implemented, + .title = translate("Method not implemented"), + .type_uri = "https://routemon.fautchen.eu/problems/method-not-implemented", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } + + if (auto mhdl = mres->route_handlers->lookup(*mverb)) { + auto new_ctx = routed_ctx{std::move(ctx), mres->allowed_methods()}; + co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); + } else { + // Path recognized, but method not allowed. + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", + }; + auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + rsp.header().set(bhttp::field::allow, mres->allowed_methods().to_string()); + co_return std::move(rsp); + } + } else { + // We do not accept any other request-target forms. + + auto problem = bad_request_tpl.instantiate(). + set_detail(translate("Invalid request-target, expected " + "asterisk-form or origin-form " + "(see RFC 9112, § 3.2)")); + co_return problem_rsp(ctx, problem, keep_alive{false}); + } + } + + auto handle_request(readable_request r) const -> net::awaitable { + auto header = r.p->get().base(); + auto locale = lsel_.select(header[bhttp::field::accept_language]); + auto ctx0 = base_ctx{.locale = locale}; + + co_return co_await global_middleware_(std::move(ctx0), header, [&](PreRouteCtx ctx) -> net::awaitable { + co_return co_await route_request(std::move(ctx), std::move(r)); + }); + } + + auto do_session(beast::tcp_stream strm) -> net::awaitable { + auto buf = beast::flat_buffer{}; + + while (true) { + auto p0 = bhttp::request_parser{}; + p0.body_limit(boost::none); + auto [ec, _] = co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); + if (ec == bhttp::error::end_of_stream) { + break; + } else if (ec) { + throw boost::system::system_error{ec}; + } + + auto http_version = p0.get().version(); + auto&& rsp = co_await handle_request(readable_request{ + .p = util::not_null{&p0}, + .strm = util::not_null{&strm}, + .buf = util::not_null{&buf}, + }); + rsp.header().version(http_version); + bool keep_alive = rsp.keep_alive(); + co_await beast::async_write(strm, std::move(rsp)); + if (!keep_alive) { + break; + } + } + + strm.socket().shutdown(tcp::socket::shutdown_send); + } + + auto do_listen(tcp::endpoint endpoint) -> net::awaitable { + auto executor = co_await net::this_coro::executor; + auto acceptor = tcp::acceptor{executor, endpoint}; + + l_.with("endpoint", endpoint.address().to_string()). + with("port", std::to_string(endpoint.port())). + info("Serving"); + while (true) { + net::co_spawn(executor, + do_session(beast::tcp_stream{co_await acceptor.async_accept()}), + [this](std::exception_ptr e) { + if (e) { + try { + std::rethrow_exception(e); + } catch (std::exception const& e) { + l_.error("Error in session: {}", e.what()); + } + } + }); + } + } + + auto spawn(net::io_context& ioc) -> void { + auto const addr = net::ip::make_address("0.0.0.0"); + auto const endpoint = tcp::endpoint{addr, 8284}; + + // TODO: make exception handling as nice as in srv.cpp + net::co_spawn(ioc, + do_listen(endpoint), + [this](std::exception_ptr e) { + if (e) { + try { + std::rethrow_exception(e); + } catch (std::exception const& e) { + l_.error("Error: {}", e.what()); + } + } + }); + } + }; + +} // namespace routemon::http diff --git a/server/src/locale.cppm b/server/src/locale.cppm new file mode 100644 index 0000000..65ae2ea --- /dev/null +++ b/server/src/locale.cppm @@ -0,0 +1,245 @@ +module; + +#include +#include + +export module routemon:locale; + +import std; +import :util; + +export namespace blocale = boost::locale; + +export namespace routemon { + using lformat = blocale::format; + using blocale::translate; + using blocale::gettext; +} // namespace routemon + +namespace routemon::locale { + + struct locale_priority { + float weight; + std::size_t original_index; + }; + + auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool { + if (lhs.weight != rhs.weight) + return lhs.weight > rhs.weight; + return lhs.original_index < rhs.original_index; + } + + struct icu_locale_hash { + std::size_t operator()(icu::Locale const& l) const noexcept { + static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); + std::int32_t hash = l.hashCode(); + if (hash < 0) { + return static_cast(std::numeric_limits::max()) + static_cast(-hash) + 1; + } else { + return static_cast(hash); + } + } + }; + + using icu_locale_priority_map = std::unordered_map; + using icu_priority_locale = std::pair; + + auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) -> bool { + return lhs.second < rhs.second; + } + + class icu_priority_locale_vec_iterator : public icu::Locale::Iterator { + std::size_t i_ = 0uz; + std::vector ls_; + + public: + explicit icu_priority_locale_vec_iterator(std::vector&& ls) + : ls_(std::move(ls)) + {} + + auto hasNext() const -> UBool override { + return i_ < ls_.size(); + } + + auto next() -> icu::Locale const& override { + return ls_[i_++].first; + } + + ~icu_priority_locale_vec_iterator() override = default; + }; + + export template + concept locale_input_range = + std::ranges::input_range && + std::same_as>; + + // Helps select a locale based on the Accept-Language header in an + // HTTP request. + export class selector { + std::locale default_; + icu::LocaleMatcher matcher_; + std::shared_ptr lgen_; + + auto make_matcher(locale_input_range auto supported_locales, std::locale default_locale) { + auto builder = icu::LocaleMatcher::Builder{}; + for (auto const& supported_locale : supported_locales) { + auto const& supported_locale_info = std::use_facet(supported_locale); + auto supported_icu_locale = icu::Locale{supported_locale_info.name().c_str()}; + if (supported_icu_locale.isBogus()) + throw std::runtime_error{"supported locale gives rise to bogus ICU locale"}; + builder.addSupportedLocale(supported_icu_locale); + } + auto const& default_locale_info = std::use_facet(default_locale); + auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; + if (default_icu_locale.isBogus()) + throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; + builder.setDefaultLocale(&default_icu_locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto matcher = builder.build(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to build icu::LocaleMatcher"}; + return matcher; + } + + // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. + static auto ltrim_ows(std::string_view s) -> std::string_view { + if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) + s.remove_prefix(i); + return s; + } + static auto rtrim_ows(std::string_view s) -> std::string_view { + if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) + return s.substr(0, i + 1); + return s; + } + static auto trim_ows(std::string_view s) -> std::string_view { + return rtrim_ows(ltrim_ows(s)); + } + + auto from_icu_locale(icu::Locale const& l) const -> std::locale { + auto posix_name = std::string{l.getLanguage()}; + if (l.getScript() && std::strlen(l.getScript()) > 0) { + posix_name += "_"; + posix_name += l.getScript(); + } + if (l.getCountry() && std::strlen(l.getCountry()) > 0) { + posix_name += "_"; + posix_name += l.getCountry(); + } + posix_name += ".UTF-8"; + auto added_at = false; + if (l.getVariant() && std::strlen(l.getVariant()) > 0) { + added_at = true; + posix_name += "@"; + posix_name += l.getVariant(); + } + auto ec = UErrorCode::U_ZERO_ERROR; + auto* keywords = l.createKeywords(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to create keywords"}; + if (keywords) { + std::int32_t kw_len = 0; + char const* kw = nullptr; + while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) { + auto value = l.getKeywordValue(icu::StringPiece(kw, kw_len), ec); + if (!added_at) { + posix_name += "@"; + added_at = true; + } else { + posix_name += ";"; + } + posix_name += kw; + posix_name += "="; + posix_name += value; + } + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to iterate over keywords"}; + delete keywords; + } + return lgen_->generate(posix_name); + } + + public: + // Note: lgen must live at least as long as the selector constructed here! + // It is unfortunately not possible to copy/move a blocale::generator. + explicit selector(locale_input_range auto locales, std::locale default_, std::shared_ptr lgen) + : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} + {} + + auto select(std::string_view accept_language) const -> std::locale { + using namespace std::literals::string_view_literals; + // NOTE: can also contain a *;q=0.1 + // q should have at most 3 digits after period + auto dlpm = icu_locale_priority_map{}; + for (auto const [i, lang_prio] : accept_language | std::views::split(","sv) | std::views::enumerate) { + auto [lang_range_ut, mweight_ut] = util::split_on(std::string_view{lang_prio}, ';'); + auto lang_range_str = trim_ows(lang_range_ut); + auto mweight_str = mweight_ut.transform(trim_ows); + if (lang_range_str == "*") + break; + + auto ec = UErrorCode::U_ZERO_ERROR; + auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); + if (U_FAILURE(ec) || icu_locale.isBogus()) + continue; // ignore this locale + + auto weight = 1.0f; + if (mweight_str && mweight_str->starts_with("q=")) { + auto weight_str = mweight_str->substr(2, 4); + if (auto mweight = util::parse_float(weight_str, std::chars_format::fixed); + mweight && 0.0f < *mweight && *mweight < 1.0f) { + weight = *mweight; + } + } + + if (weight > 0.0f) { + dlpm[icu_locale] = { + .weight = weight, + .original_index = static_cast(i), + }; + } else { + dlpm.erase(icu_locale); + } + } + + auto desired_locales = std::vector{dlpm.begin(), dlpm.end()}; + std::sort(desired_locales.begin(), desired_locales.end()); + auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; + auto ec = UErrorCode::U_ZERO_ERROR; + auto res = matcher_.getBestMatchResult(it, ec); + if (U_FAILURE(ec)) + return default_; + auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? + if (U_FAILURE(ec)) + return from_icu_locale(*res.getSupportedLocale()); + return from_icu_locale(resolved); + } + }; + + export auto to_bcp47_lang_tag(std::locale locale) -> std::optional { + auto const& locale_info = std::use_facet(locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto bcp47_lang_tag = icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); + if (U_FAILURE(ec)) + return std::nullopt; + return bcp47_lang_tag; + } + +#ifdef LOCALEDIR +# define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) +# define LOCALEDIR_AUX_STR(s) #s + constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; +# undef LOCALEDIR_AUX_STR +# undef LOCALEDIR_AUX_XSTR +#else // ifdef LOCALEDIR + constexpr auto messages_path = std::string_view{"locale/dev"}; +#endif // ifdef LOCALEDIR + + export auto make_generator() -> std::shared_ptr { + auto lgen = std::make_shared(); + lgen->add_messages_path(std::string{messages_path}); + lgen->add_messages_domain("routemon"); + return std::static_pointer_cast(lgen); + } + +} // namespace routemon::locale diff --git a/server/src/log.cppm b/server/src/log.cppm new file mode 100644 index 0000000..d7e2bff --- /dev/null +++ b/server/src/log.cppm @@ -0,0 +1,148 @@ +module; + +// Seems like ADL for std::quoted is broken with +// import std; +// Might be because the _Quoted_string object is defined in +// std::__detail, which is not exported by the module. +#include + +export module routemon:log; + +import std; + +namespace routemon::log { + + export enum class level : std::uint8_t { + debug, + info, + warn, + error, + }; + + namespace { + + auto operator<<(std::ostream& os, level lvl) -> std::ostream& { + switch (lvl) { + case level::debug: os << "dbg"; break; + case level::info: os << "inf"; break; + case level::warn: os << "wrn"; break; + case level::error: os << "err"; break; + } + return os; + } + + } // namespace (unique) + + export class sink { + std::atomic lvl_; + std::ostream& os_ = std::cout; + + struct tmp_message { + level lvl; + std::string_view component; + std::string_view txt; + std::map const& attrs; + }; + + auto write(tmp_message msg) -> void { + auto sos = std::osyncstream{os_}; + sos << "[" << msg.lvl; + if (!msg.component.empty()) + sos << " " << msg.component; + sos << "] " << msg.txt; + for (auto const& [k, v] : msg.attrs) { + sos << " " << k << "=" << std::quoted(v); + } + sos << '\n'; + } + + explicit sink(level lvl) + : lvl_{lvl} + {} + + friend auto make_sink(level lvl) -> std::shared_ptr; + friend class logger; + + public: + [[nodiscard]] auto level() const -> enum level { + return lvl_; + } + + auto set_level(enum level lvl) -> void { + lvl_ = lvl; + } + }; + + export auto make_sink(level lvl) -> std::shared_ptr { + return std::shared_ptr{new sink{lvl}}; + } + + export class logger { + std::shared_ptr sink_; + std::string component_; + std::map attrs_; + + template + auto log_at(std::string_view fmt, std::format_args args) -> logger& { + if (sink_->level() <= lvl) + sink_->write(sink::tmp_message{ + .lvl = lvl, + .component = component_, + .txt = std::vformat(fmt, args), + .attrs = attrs_, + }); + return *this; + } + + public: + explicit logger(std::shared_ptr const& sink) + : sink_{sink} + { + if (!sink) { + throw std::invalid_argument{"logger sink may not be null"}; + } + } + + [[nodiscard]] auto sub(std::string_view component) const -> logger { + auto l = *this; + if (l.component_.empty()) { + l.component_ = component; + } else { + l.component_ += "."; + l.component_ += component; + } + return l; + } + + [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger { + auto l = *this; + l.attrs_[k] = std::move(v); + return l; + } + + [[nodiscard]] auto with(std::string const& k, std::string_view v) const -> logger { + return with(k, std::string{v}); + } + + template + auto debug(std::format_string fmt, Args&&... args) -> logger& { + return log_at(fmt.get(), std::make_format_args(args...)); + } + + template + auto info(std::format_string fmt, Args&&... args) -> logger& { + return log_at(fmt.get(), std::make_format_args(args...)); + } + + template + auto warn(std::format_string fmt, Args&&... args) -> logger& { + return log_at(fmt.get(), std::make_format_args(args...)); + } + + template + auto error(std::format_string fmt, Args&&... args) -> logger& { + return log_at(fmt.get(), std::make_format_args(args...)); + } + }; + +} // namespace routemon::log diff --git a/server/src/main.cpp b/server/src/main.cpp new file mode 100644 index 0000000..a40b0b0 --- /dev/null +++ b/server/src/main.cpp @@ -0,0 +1,109 @@ +#include // for malloc_trim(3) +#include + +import std; +import routemon; + +namespace chrono = std::chrono; +namespace net = boost::asio; + +enum class exit_status { + failure, + bad_usage, +}; + +auto real_main(std::span args) -> exit_status { + auto sink = routemon::log::make_sink(routemon::log::level::info); + auto l = routemon::log::logger{sink}; + + if (args.size() != 2) { + l.error("Fatal: expected exactly one argument (the configuration file location), got {}", args.size() - 1); + return exit_status::bad_usage; + } + auto const* config_filename = args[1]; + + auto lgen = routemon::locale::make_generator(); + auto default_locale = lgen->generate("en_US.UTF-8"); + auto locales = { + default_locale, + lgen->generate("nl_NL.UTF-8"), + lgen->generate("de_DE.UTF-8"), + lgen->generate("en_GB.UTF-8"), + }; + auto lsel = routemon::locale::selector{locales, default_locale, lgen}; + + auto ioc = net::io_context{1 /* concurrency hint */}; + + auto config = routemon::config::app{}; + try { + config = routemon::config::load_file(config_filename); + } catch (std::exception const& e) { + l.with("filename", std::string_view{config_filename}). + error("Failed to load configuration: {}", e.what()); + return exit_status::failure; + } + sink->set_level(config.logger.level); + // auto rwgps_client = routemon::rwgps::client{ioc, l, config.rwgps.api_key, config.rwgps.auth_token}; + // for (auto route : rwgps_client.get_all_routes()) { + // l.info("Route {} (user {}): {} @ {}", route.id, route.user_id, route.name, route.url); + // } + + auto dbc = std::shared_ptr{}; + try { + dbc = routemon::database::open(config.database.sqlite3_filename); + } catch (std::exception const& e) { + l.with("filename", config.database.sqlite3_filename). + error("Failed to open database: {}", e.what()); + return exit_status::failure; + } + + l.with("filename", config.situations.datex2_filename). + info("Loading situations"); + auto const before_load = chrono::steady_clock::now(); + auto d2loader = routemon::datex2::loader{}; + auto pub = routemon::datex2::situation_publication{}; + try { + pub = d2loader.load_situation_publication(config.situations.datex2_filename); + } catch (std::exception const& e) { + l.error("Failed to load DATEX II situations publication: {}", e.what()); + return exit_status::failure; + } + if (!d2loader.warnings().empty()) { + auto const& warns = d2loader.warnings(); + l.warn("Encountered {} unique warnings while loading DATEX II situations publication", warns.size()); + auto i = 0uz; + for (auto it = warns.begin(); it != warns.end(); it = warns.upper_bound(*it)) { + l.warn("Warning {} (appeared {}×): {}", ++i, warns.count(*it), *it); + } + } + // Processing the feed is by far the most memory-intensive operation + // during the run time of this application (at the moment), the + // resident set will likely never be this big again. So we ask libc + // to return as much memory as possible to the OS. + malloc_trim(0); + auto const after_load = chrono::steady_clock::now(); + auto const dur_load = chrono::duration_cast(after_load - before_load); + l.info("Loading situations finished in {}", dur_load); + + auto handler = routemon::api::handler{l, std::move(pub)}; + auto http_server = routemon::srv::server{l, std::move(lsel), std::move(handler)}; + http_server.spawn(ioc); + ioc.run(); + + l.error("I/O context stopped"); + return exit_status::failure; +} + +auto main(int argc, char* argv[]) -> int { + if (argc < 0) { + std::cout << "Fatal: argument count below zero" << std::endl; + return EXIT_FAILURE; + } + auto res = real_main(std::span{const_cast(argv), static_cast(argc)}); + switch (res) { + case exit_status::failure: + return EXIT_FAILURE; + case exit_status::bad_usage: + return 2; + } +} diff --git a/server/src/problem.cppm b/server/src/problem.cppm new file mode 100644 index 0000000..8764962 --- /dev/null +++ b/server/src/problem.cppm @@ -0,0 +1,60 @@ +module; + +#include +#include +#include + +export module routemon:problem; + +import std; +import :locale; + +namespace http = boost::beast::http; +namespace json = boost::json; + +namespace routemon::problem { + + export struct details { + http::status status; + blocale::message title; + std::string_view type_uri; + std::optional detail = std::nullopt; + std::optional instance = std::nullopt; + + auto set_detail(blocale::message detail) -> details& { + this->detail = detail; + return *this; + } + + auto set_instance(std::string&& instance) -> details& { + this->instance = instance; + return *this; + } + auto set_instance(std::string_view instance) -> details& { + this->instance = std::string{instance}; + return *this; + } + }; + + export auto tag_invoke(json::value_from_tag, json::value& jv, details const& details, std::locale locale) -> void { + auto obj = json::object{ + {"type", details.type_uri}, + {"title", details.title.str(locale)}, + {"status", static_cast(details.status)}, + }; + if (details.detail) obj["detail"] = details.detail->str(locale); + if (details.instance) obj["instance"] = *details.instance; + jv = obj; + } + + export struct tpl { + http::status status; + blocale::message title; + std::string_view type_uri; + + auto instantiate() const -> details { + return details{status, title, type_uri}; + } + }; + +} diff --git a/server/src/req_ctx.cppm b/server/src/req_ctx.cppm new file mode 100644 index 0000000..797c51d --- /dev/null +++ b/server/src/req_ctx.cppm @@ -0,0 +1,51 @@ +module; + +#include +#include + +export module routemon:req_ctx; + +import :http.common; +import :trace; + +namespace routemon { + + export class req_ctx { + trace::id tid_; + std::locale locale_; + http::verb_set route_verbs_; + bool keep_alive_; + bhttp::request_header const& req_header_; + + public: + explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bool keep_alive, bhttp::request_header const& req_header) + : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, keep_alive_{keep_alive}, req_header_{req_header} + {} + + template + explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bhttp::request const& req) + : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} + {} + + auto trace_id() const -> trace::id { + return tid_; + } + + auto locale() const -> std::locale { + return locale_; + } + + auto route_verbs() const -> http::verb_set { + return route_verbs_; + } + + auto keep_alive() const -> bool { + return keep_alive_; + } + + auto req_header() const -> bhttp::request_header const& { + return req_header_; + } + }; + +} // namespace routemon diff --git a/server/src/routemon.cppm b/server/src/routemon.cppm new file mode 100644 index 0000000..62db02a --- /dev/null +++ b/server/src/routemon.cppm @@ -0,0 +1,11 @@ +export module routemon; +export import :api; +export import :config; +export import :database; +export import :datex2; +export import :gpx; +export import :locale; +export import :log; +export import :srv; +export import :rwgps; +export import :util; diff --git a/server/src/rwgps.cppm b/server/src/rwgps.cppm new file mode 100644 index 0000000..7ad6605 --- /dev/null +++ b/server/src/rwgps.cppm @@ -0,0 +1,137 @@ +module; + +#include +#include +#include + +export module routemon:rwgps; + +import std; +import :http.client; +import :log; + +namespace beast = boost::beast; +namespace bhttp = beast::http; +namespace net = boost::asio; +namespace json = boost::json; + +namespace routemon::rwgps { + + export struct route_summary { + std::int64_t id; + std::int64_t user_id; + std::string url; + std::string name; + std::string description; + }; + + struct pagination { + std::size_t record_count; + std::size_t page_count; + std::size_t page_size; + std::optional next_page_url; + }; + + struct get_routes_meta { + pagination pagination; + }; + + struct get_routes_response { + std::vector routes; + get_routes_meta meta; + }; + + auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> route_summary { + return { + .id = json::value_to(jv.at("id")), + .user_id = json::value_to(jv.at("user_id")), + .url = json::value_to(jv.at("url")), + .name = json::value_to(jv.at("name")), + .description = json::value_to(jv.at("description")), + }; + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> pagination { + return { + .record_count = json::value_to(jv.at("record_count")), + .page_count = json::value_to(jv.at("page_count")), + .page_size = json::value_to(jv.at("page_size")), + .next_page_url = json::value_to>(jv.at("next_page_url")), + }; + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> get_routes_meta { + return { + .pagination = json::value_to(jv.at("pagination")), + }; + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> get_routes_response { + return { + .routes = json::value_to>(jv.at("routes")), + .meta = json::value_to(jv.at("meta")), + }; + } + + auto json_value_to_get_routes_response(json::value const& jv) -> get_routes_response { + return json::value_to(jv); + } + + export class client { + log::logger l_; + http::client hc_; + std::string api_key_; + std::string auth_token_; + + static constexpr std::string host = "ridewithgps.com"; + + // TODO: handle failure appropriately + auto get_routes_page(std::size_t page) -> get_routes_response { + auto req = bhttp::request{ + bhttp::verb::get, + std::format("/api/v1/routes.json?page_size=200?page={}", page), + 11, // HTTP 1.1 + }; + req.set(bhttp::field::host, host); + req.set("x-rwgps-api-key", api_key_); + req.set("x-rwgps-auth-token", auth_token_); + + auto rsp = hc_.do_request(req); + auto p = json::stream_parser{}; + for (auto const frag : rsp.body().cdata()) { + p.write(static_cast(frag.data()), frag.size()); + } + assert(p.done()); + return json_value_to_get_routes_response(p.release()); + } + + public: + explicit client(net::io_context& ioc, log::logger const& l, std::string api_key, std::string auth_token) + : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, auth_token_{std::move(auth_token)} + {} + + auto get_all_routes() -> std::vector { + // TODO: make sure that there are no duplicates here. + // What does RWGPS sort on, by default? + // Consider using an associative container instead of a vector. + auto record_count = 0uz; + auto current_page = 0uz; + auto routes = std::vector{}; + + while (true) { + auto rsp = get_routes_page(current_page); + if (rsp.meta.pagination.next_page_url) + l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); + routes.append_range(rsp.routes); + if (rsp.meta.pagination.record_count > 0) + record_count = rsp.meta.pagination.record_count; + if (rsp.routes.empty() || routes.size() >= record_count) { + break; + } + } + + return routes; + } + }; + +} // namespace routemon::rwgps diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm new file mode 100644 index 0000000..f226c6b --- /dev/null +++ b/server/src/sqlite3.cppm @@ -0,0 +1,257 @@ +module; + +#include + +export module routemon:sqlite3; + +import std; +import :util; + +namespace routemon::sqlite3 { + + class mutex_guard { + explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} { + ::sqlite3_mutex_enter(mut_); + } + + friend auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) -> decltype(f(std::declval())); + + public: + mutex_guard(mutex_guard const&) = delete; + ~mutex_guard() { + ::sqlite3_mutex_leave(mut_); + } + + private: + ::sqlite3_mutex* mut_; + }; + + auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) -> decltype(f(std::declval())) { + return f(mutex_guard{mut}); + } + + auto do_guarded(::sqlite3* dbc, std::invocable auto f) -> decltype(f(std::declval())) { + return do_guarded(::sqlite3_db_mutex(dbc), f); + } + + class error : public std::exception { + int code_; + std::string message_; + + public: + explicit error(mutex_guard const&, int code, ::sqlite3* dbc) + : code_{code}, message_{::sqlite3_errmsg(dbc)} + {} + + explicit error(int code) + : code_{code}, message_{::sqlite3_errstr(code)} + {} + + [[nodiscard]] auto what() const noexcept -> char const* override { + return message_.c_str(); + } + + [[nodiscard]] auto code() const noexcept -> int { + return code_; + } + }; + + template concept C> + concept optional_of = requires { + typename T::value_type; + requires std::same_as>; + requires C; + }; + + template + concept scannable_prim = + std::same_as || + std::same_as || + std::same_as; + + template + concept scannable = scannable_prim || optional_of; + + class statement { + ::sqlite3_stmt* stmt_; + + public: + explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} + statement(statement const&) = delete; + statement(statement&& s) noexcept { + stmt_ = s.stmt_; + s.stmt_ = nullptr; + } + ~statement() { + ::sqlite3_finalize(stmt_); + } + auto get() -> ::sqlite3_stmt* { + return stmt_; + } + }; + + class row_reader { + statement stmt_; + + explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} + + friend class connection; + + void scan(int col, std::string& s) { + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) + throw std::invalid_argument{"invalid type for scan"}; + unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); + auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); + if (!size.has_value()) + throw std::logic_error{"unexpected negative amount of bytes in column"}; + s = std::string{reinterpret_cast(chs), *size}; + } + + void scan(int col, double& v) { + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) + throw std::invalid_argument{"invalid type for scan"}; + v = ::sqlite3_column_double(stmt_.get(), col); + } + + void scan(int col, std::int64_t& v) { + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) + throw std::invalid_argument{"invalid type for scan"}; + v = ::sqlite3_column_int64(stmt_.get(), col); + } + + void scan(int col, optional_of auto& v) { + if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) { + v.reset(); + } else { + typename std::remove_cvref_t::value_type tmp; + scan(col, tmp); + v = std::move(tmp); + } + } + + public: + auto next() -> bool { + ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); + return do_guarded(dbc, [&](auto const& guard) -> bool { + auto const s = ::sqlite3_step(stmt_.get()); + if (s == SQLITE_ROW) + return true; + if (s == SQLITE_DONE) + return false; + throw error{guard, s, dbc}; + }); + } + + auto scan(scannable auto&... args) -> void { + auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); + if (!ncols.has_value()) + throw std::logic_error{"got unexpected negative amount of columns"}; + if (sizeof...(args) > *ncols) + throw std::invalid_argument{"more scanning arguments provided than columns in result set"}; + auto col = 0; (..., scan(col++, args)); + } + + auto scan_single(scannable auto&... args) -> void { + if (!next()) + throw std::logic_error{"no row in result set"}; + scan(args...); + if (next()) { + throw std::logic_error{"more than one row in result set"}; + } + } + }; + + class binder { + statement& stmt_; + + explicit binder(statement& stmt) : stmt_{stmt} {} + + friend class connection; + + public: + auto text(std::string const& param_name, std::string_view str) -> void { + int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); + if (i == 0) + throw std::invalid_argument{std::format("bind: no parameter with name {} found", param_name)}; + auto str_size = util::int_from_size(str.size()); + if (!str_size.has_value()) + throw std::invalid_argument{"bind: provided text is too long"}; + if (auto s = ::sqlite3_bind_text(stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); s != SQLITE_OK) { + throw error{s}; + } + } + + static auto noop(binder&) -> void {} + }; + + export class connection { + ::sqlite3* dbc_; + ::sqlite3_mutex* mut_; + + explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} {} + + friend auto open(std::string const& filename) -> connection; + + public: + connection(connection const&) = delete; + connection(connection&& c) noexcept { + dbc_ = c.dbc_; + mut_ = c.mut_; + c.dbc_ = nullptr; + c.mut_ = nullptr; + } + + [[nodiscard]] auto query(std::string const& sql, std::function const& bf = binder::noop) -> row_reader { + ::sqlite3_stmt* pstmt = nullptr; + char const* sql_tail = nullptr; + auto sql_size = util::int_from_size(sql.size()); + if (!sql_size.has_value() || *sql_size >= std::numeric_limits::max() - 1) + throw std::invalid_argument{"provided input text too large"}; + do_guarded(mut_, [&](auto const& guard) -> void { + if (auto s = ::sqlite3_prepare_v2(dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); s != SQLITE_OK) { + if (pstmt != nullptr) { + // Use contract_assert when having a compiler with contracts available + ::sqlite3_finalize(pstmt); + throw std::logic_error{"expected stmt to be null after failed preparation"}; + } + throw error{guard, s, dbc_}; + } + }); + if (!pstmt) + throw std::invalid_argument{"provided input text contains no SQL"}; + auto stmt = statement{pstmt}; + if (sql_tail && std::strlen(sql_tail) > 0) + throw std::invalid_argument{"provided input text contains more than one SQL statement"}; + auto b = binder{stmt}; bf(b); + return row_reader{std::move(stmt)}; + } + + auto exec(std::string const& sql, std::function const& bf = binder::noop) -> void { + auto reader = query(sql, bf); + while (reader.next()); + } + + ~connection() { + std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); + } + }; + + export auto open(std::string const& filename) -> connection { + ::sqlite3* dbc = nullptr; + auto s = ::sqlite3_open_v2(filename.c_str(), &dbc, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE, + nullptr); + if (s != SQLITE_OK) { + if (dbc) { + do_guarded(dbc, [&](auto const& guard) -> void { + throw error{guard, s, dbc}; + }); + } else { + throw error{s}; + } + } + return connection{dbc}; + } + +} // namespace routemon::sqlite3 diff --git a/server/src/srv.cppm b/server/src/srv.cppm new file mode 100644 index 0000000..2ca48c6 --- /dev/null +++ b/server/src/srv.cppm @@ -0,0 +1,221 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +export module routemon:srv; + +import std; +import :api; +import :config; +import :gpx; +import :http.server; +import :locale; +import :log; +import :problem; +import :req_ctx; +import :util; + +namespace beast = boost::beast; +namespace json = boost::json; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; + +namespace routemon::srv { + + class gpx_parse_error_category_impl : public std::error_category { + public: + char const* name() const noexcept override { return "gpx_parse"; } + auto message(int condition) const noexcept -> std::string override { + std::ignore = condition; + return "failed to parse GPX file"; + } + }; + auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& { + static auto const inst = gpx_parse_error_category_impl{}; + return inst; + } + auto gpx_parse_error() noexcept -> std::error_code { + return std::error_code{1, gpx_parse_error_category()}; + } + + class gpx_parse_result { + std::variant res_; + + public: + auto set_exception(std::exception_ptr ex) noexcept { + res_ = ex; + } + auto set_gpx_file(gpx::file&& f) noexcept { + res_ = std::move(f); + } + + auto unwrap() -> gpx::file&& { + return std::visit(util::overloaded{ + [](std::exception_ptr ex) -> gpx::file&& { + if (ex) std::rethrow_exception(ex); + else throw std::runtime_error{"no GPX file parse result available"}; + }, + [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, + }, std::move(res_)); + } + }; + + struct readable_gpx_body { + using value_type = gpx_parse_result; + + class reader { + gpx::reader r_; + util::not_null res_; + + public: + template + explicit reader(bhttp::header&, value_type& v) + : res_{&v} + {} + + // The following methods (which are called by Beast) are marked + // noexcept, since Beast does not ensure that exceptions thrown + // here are appropriately directed to the caller of + // (async_)read(_some), so throwing here might cause the program + // to crash. + + auto init(boost::optional /* n */, beast::error_code& ec) noexcept -> void { + try { + r_.init(); + ec = {}; + } catch (std::exception& ex) { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); + } + } + + auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t { + auto total = 0uz; + try { + for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) { + r_.put(std::string_view{static_cast(it->data()), it->size()}); + total += it->size(); + } + ec = {}; + } catch (std::exception& ex) { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); + } + return total; + } + + auto finish(beast::error_code& ec) noexcept { + try { + res_->set_gpx_file(r_.finish()); + ec = {}; + } catch (std::exception& ex) { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); + } + } + }; + }; + static_assert(bhttp::concepts::body); + static_assert(bhttp::concepts::body_reader); + + class handler { + api::handler inner_; + + public: + using outer_ctx = http::trace_id_ctx; + using l0_ctx = http::routed_ctx; + + private: + auto handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable { + auto gpx_file = gpx::file{}; + try { + auto req = co_await http::read_request(ctx, std::move(r)); + gpx_file = std::move(req->body().unwrap()); + } catch (std::exception& ex) { + // TODO: more detailed problem reporting + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Failed to parse GPX file"), + .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", + }; + co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{false}); + } + + // TODO: catch handler exceptions and return 500 when raised? + // (keep-alive depends on whether whole request was read) + auto mres = inner_.process_gpx(std::move(gpx_file)); + if (!mres) { + auto tpl = problem::tpl{ + .status = bhttp::status::internal_server_error, + .title = translate("Internal server error"), + .type_uri = "https://routemon.fautchen.eu/problems/internal-server-error", + }; + co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true}); + } + + auto rsp = http::make_rsp(bhttp::status::ok, http::keep_alive{true}); + rsp.set(bhttp::field::content_type, "application/json"); + rsp.body() = json::serialize(json::value_from(*mres)); + rsp.prepare_payload(); + co_return rsp; + } + + auto handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable { + auto req = co_await http::read_request(ctx, std::move(r)); + auto info = inner_.sysinfo(); + + auto rsp = http::make_rsp(bhttp::status::ok, http::keep_alive{true}); + rsp.set(bhttp::field::content_type, "application/json"); + rsp.body() = json::serialize(json::value_from(info)); + rsp.prepare_payload(); + co_return rsp; + } + + public: + handler(api::handler&& inner) : inner_{std::move(inner)} {} + + auto make_routes() -> http::route_tree> { + auto handler = [this](MemFn member) { + return std::bind_front(member, this); + }; + + return http::dtree>{}.named_subtrees({ + {"gpx", http::dtree{{ + .post = handler(&handler::handle_process_gpx), + }}.no_subtrees()}, + {"sysinfo", http::dtree{{ + .get = handler(&handler::handle_sysinfo), + }}.no_subtrees()}, + }); + } + }; + + export class server { + handler handler_; + http::server srv_; + + static auto make_global_middleware() -> http::middleware_t { + return http::middleware_compose, http::trace_id_ctx>(http::trace_id_middleware, http::lax_cors_middleware>); + } + + public: + server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) + : handler_{std::move(inner)}, srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} + {} + + auto spawn(net::io_context& ioc) -> void { + srv_.spawn(ioc); + } + }; + +} // namespace routemon::srv diff --git a/server/src/time.cppm b/server/src/time.cppm new file mode 100644 index 0000000..767883c --- /dev/null +++ b/server/src/time.cppm @@ -0,0 +1,205 @@ +export module routemon:time; + +import std; + +export namespace routemon::time { + + using timestamp = std::chrono::time_point; + + class period { + // Assuming [start, end). Unfortunately the DATEX II model is not + // clear about this. + timestamp start_; + timestamp end_; + + public: + explicit period(timestamp start, timestamp end) + : start_{start}, end_{end} + { + if (start >= end) { + throw std::invalid_argument("period: start should be before end"); + } + } + + [[nodiscard]] auto intersect(period other) const -> std::optional { + auto const new_start = start_ < other.start() ? other.start() : start_; + auto const new_end = other.end() < end_ ? other.end() : end_; + return new_start < new_end ? std::make_optional(period{new_start, new_end}) : std::nullopt; + } + + [[nodiscard]] auto except(period other) const -> std::pair, std::optional> { + auto const before_start = start_; + auto const before_end = other.start(); + auto const after_start = end_; + auto const after_end = other.end(); + std::optional before, after; + if (before_start < before_end) + before = period{before_start, before_end}; + if (after_start < after_end) + after = period{after_end, after_start}; + return std::make_pair(before, after); + } + + [[nodiscard]] auto start() const -> timestamp { return start_; } + [[nodiscard]] auto end() const -> timestamp { return end_; } + }; + + class period_seq { + std::vector periods_; + + // The way lt and ge are ordered makes a difference for how the sorting + // (insertion based on lower_bound) works. Do not carelessly reorder this. + enum lt_ge : std::uint8_t { + ge, // >= + lt, // < + }; + + // O(n log n) + template S> + requires std::same_as, period> + static auto consolidate(I begin, S end) -> std::vector { + auto periods = std::vector{}; + auto preds = std::vector>{}; + + for (auto it = begin; it != end; it++) { + auto const& period = *it; + + auto const a = std::make_pair(period.start(), ge); + auto const b = std::make_pair(period.end(), lt); + preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); + preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); + } + + if (preds.empty()) + return periods; + + if (preds.size() < 2) + throw std::logic_error{"period_seq::consolidate: amount of predicates should be >= 2"}; + if (preds.front().second != ge) + throw std::logic_error{"period_seq::consolidate: first element of preds should be a ge-element"}; + if (preds.back().second != lt) + throw std::logic_error{"period_seq::consolidate: last element of preds should be an lt-element"}; + + auto period_start = preds[0].first; + for (std::size_t i = 1; i < preds.size(); i++) { + if (preds[i].second == lt && (i + 1 == preds.size() || preds[i + 1].second == ge)) { + auto const period_end = preds[i].first; + if (!periods.empty() && periods.back().start() == period_start) + periods.back() = period{periods.back().end(), period_end}; + else + periods.emplace_back(period_start, period_end); + if (i + 1 != preds.size()) { + period_start = preds[i + 1].first; + i++; + } + } + } + + return periods; + } + + explicit period_seq(std::vector periods) + : periods_{std::move(periods)} + { + for (auto i = 0uz; i < periods_.size(); i++) { + if (i + 1 < periods_.size()) { + if (periods_[i].end() >= periods_[i + 1].start()) { + throw std::logic_error{"period_seq: vector provided to private constructor not ordered properly"}; + } + } + } + } + + public: + template S> + requires std::same_as, period> + explicit period_seq(I begin, S end) + : periods_{consolidate(begin, end)} + {} + + explicit period_seq(period singleton) + : periods_{singleton} + {} + + [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq { + auto it1 = periods_.begin(); auto end1 = periods_.end(); + auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); + + auto res = std::vector{}; + while (it1 != end1 && it2 != end2) { + auto overlap = it1->intersect(*it2); + if (overlap) { + res.push_back(*overlap); + if (it1->end() < it2->end()) { + it1++; + } else { + it2++; + } + } else { + if (it1->end() < it2->start()) { + it1++; + } else { + it2++; + } + } + } + + return period_seq{res}; + } + + [[nodiscard]] auto except(period_seq const& other) const -> period_seq { + // This code was pretty tricky to write, I wouldn't be surprised if it has some bugs in it. + + auto it1 = periods_.begin(); auto end1 = periods_.end(); + auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); + + auto res = std::vector{}; + if (it1 == end1) + return period_seq{res}; + if (it2 == end2) + return period_seq{periods_}; + auto period1 = period{*it1++}; + + while (it1 != end1 && it2 != end2) { + if (period1.end() <= it2->start()) { + res.push_back(period1); + period1 = *it1++; + } else if (it2->end() <= period1.start()) { + it2++; + } else /* period1.begin() < it2->end() && it2->begin() < period1.end() */ { + auto const [mbefore, mafter] = period1.except(*it2); + if (mbefore) + res.push_back(*mbefore); + if (mafter) { + period1 = *mafter; + } else { + period1 = *it1++; + } + } + } + + return period_seq{res}; + } + + [[nodiscard]] auto periods() const -> std::vector const& { + return periods_; + } + }; + + auto operator<<(std::ostream& os, period const& p) -> std::ostream& { + return os << "[" << p.start() << ", " << p.end() << ")"; + } + + auto operator<<(std::ostream &os, period_seq const& ps) -> std::ostream& { + os << "{"; + auto it = ps.periods().begin(); + while (it != ps.periods().end()) { + os << " " << *it; + if (++it != ps.periods().end()) { + os << ","; + } + } + return os << " }"; + } + +} // namespace routemon::time diff --git a/server/src/trace.cppm b/server/src/trace.cppm new file mode 100644 index 0000000..00ecd05 --- /dev/null +++ b/server/src/trace.cppm @@ -0,0 +1,79 @@ +module; + +// Might as well since we're using OpenSSL +#include +#include + +export module routemon:trace; + +import std; +import :util; + +namespace routemon::trace { + + class uuid7 { + std::uint64_t high_ = 0; + std::uint64_t low_ = 0; + + public: + uuid7() { + namespace chrono = std::chrono; + auto const unix_time_ms_signed = static_cast(chrono::duration_cast(chrono::system_clock::now().time_since_epoch()).count()); + if (unix_time_ms_signed < 0) + throw std::runtime_error{"system time before UNIX epoch"}; + auto const unix_time_ms = static_cast(unix_time_ms_signed); + if (std::countl_zero(unix_time_ms) < 16) + throw std::runtime_error{"system time too great"}; + + auto rand = std::array{}; + int s = RAND_bytes(rand.data(), static_cast(rand.size())); + if (s != 1) { + unsigned long e = ERR_get_error(); + throw std::runtime_error{std::format("failed to generate UUID(v7): {} ({}, code {})", ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; + } + + auto version = std::uint64_t{0b0111}; + auto variant = std::uint64_t{0b10}; + + high_ |= unix_time_ms << 16; + high_ |= version << 12; + high_ |= std::uint64_t{rand[0]} << 4; + high_ |= std::uint64_t{rand[1]}; + low_ |= variant << 62; + low_ |= std::uint64_t{rand[2]} << 54; + low_ |= std::uint64_t{rand[3]} << 48; + low_ |= std::uint64_t{rand[4]} << 40; + low_ |= std::uint64_t{rand[5]} << 32; + low_ |= std::uint64_t{rand[6]} << 24; + low_ |= std::uint64_t{rand[7]} << 16; + low_ |= std::uint64_t{rand[8]} << 8; + low_ |= std::uint64_t{rand[9]}; + } + + auto format(std::array& target) -> void { + auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; + auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; + auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; + auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; + auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; + + std::format_to(target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", + high_high, high_low_high, low_low_high, high_low, low_low); + target.back() = '\0'; + } + }; + + export class id { + std::array chars_; + + public: + id() { + uuid7{}.format(chars_); + } + + auto as_string() const -> util::zstring_view { + return util::zstring_view{chars_.data(), chars_.size() - 1}; + } + }; + +} // namespace routemon::trace diff --git a/server/src/util.cppm b/server/src/util.cppm new file mode 100644 index 0000000..65f4d67 --- /dev/null +++ b/server/src/util.cppm @@ -0,0 +1,254 @@ +// Stuff that doesn't really have a place right now, but that is +// broadly useful. +export module routemon:util; + +import std; + +namespace routemon::util { + + // For use with e.g. std::visit (on std::variant). + template + struct overloaded : Ts... { + using Ts::operator()...; + }; + + export constexpr auto parse_double(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional { + auto x = 0.0; + auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); + if (ec == std::errc{}) { + return x; + } else { + return std::nullopt; + } + } + + export constexpr auto parse_float(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional { + auto x = 0.0; + auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); + if (ec == std::errc{}) { + return x; + } else { + return std::nullopt; + } + } + + export template + class aolist : public std::enable_shared_from_this> { + T v_; + std::shared_ptr const> next_; + + explicit aolist(T v, std::shared_ptr const> next) + : v_{v}, next_{next} + {} + + public: + static auto nil() -> std::shared_ptr> { + return nullptr; + } + + static auto cons(T v, std::shared_ptr const> l) -> std::shared_ptr> { + return std::shared_ptr>{new aolist{v, l}}; + } + + auto next() const -> std::shared_ptr const> { + return next_; + } + + auto value() const noexcept -> T const& { + return v_; + } + }; + + export constexpr auto size_from_int(int x) -> std::optional { + static_assert(sizeof(int) <= sizeof(std::size_t), "cannot cast int to smaller size_t type"); + if (x < 0) + return std::nullopt; + return static_cast(x); + } + + export constexpr auto int_from_size(std::size_t x) -> std::optional { + constexpr auto int_max = size_from_int(std::numeric_limits::max()); + static_assert(int_max.has_value()); + if (x > *int_max) + return std::nullopt; + return static_cast(x); + } + + export class zstring_view { + char const* s_; + std::size_t length_; + + public: + constexpr explicit zstring_view(char const* s, std::size_t length) : + s_{s}, length_{length} + {} + + constexpr zstring_view(char const* s) : + zstring_view{s, std::char_traits::length(s)} + {} + + auto length() const -> std::size_t { + return length_; + } + + auto c_str() const -> char const* { + return s_; + } + + operator std::string_view() const { + return std::string_view{s_, length_}; + } + + operator char const*() const { + return s_; + } + }; + + // View for null-terminated strings for which we might not + // necessarily be interested in the length. String length is only + // calculated on demand, at most once on each thread (on more than + // one thread when racing). May be null. + // + // It is undefined behavior to assign to a lazy_zstring_view when + // it is in use by other threads. + export class lazy_zstring_view { + static constexpr auto unset_length = std::numeric_limits::max(); + + char const* s_; // nullable + mutable std::atomic length_; + static_assert(decltype(length_)::is_always_lock_free); + + public: + constexpr explicit lazy_zstring_view(char const* s) : + s_{s}, length_{s ? unset_length : 0} + {} + ~lazy_zstring_view() = default; + + lazy_zstring_view(lazy_zstring_view const& sv) noexcept + : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} + {} + lazy_zstring_view(lazy_zstring_view&& sv) noexcept + : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} + {} + auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& { + if (this != &rhs) { + s_ = rhs.s_; + length_.store(rhs.length_.load(std::memory_order_acquire), std::memory_order_release); + } + return *this; + } + auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& { + return *this = rhs; // use the copy assignment operator + } + + auto length() const noexcept -> std::size_t { + if (auto v = length_.load(std::memory_order_acquire); v != unset_length) + return v; + auto l = std::char_traits::length(s_); + length_.store(l, std::memory_order_release); + return l; + } + + auto c_str() const noexcept -> char const* { + return s_; + } + + operator std::string_view() const noexcept { + return std::string_view{s_, length()}; + } + + operator char const*() const noexcept { + return s_; + } + + auto operator==(std::string_view sv) const noexcept -> bool { + if (auto v = length_.load(std::memory_order_acquire); v != unset_length) + if (sv.length() != v) + return false; + auto res = std::char_traits::compare(s_, sv.data(), sv.length()); + if (res != 0) + return false; + // Strings are equal for sv.length() characters. + if (s_[sv.length()] != '\0') + return false; + // Strings are actually equal, and we have just found out the + // length of this string, so we might as well set it. + length_.store(sv.length(), std::memory_order_release); + return true; + } + }; + + constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept -> zstring_view { + return zstring_view{s, length}; + } + + auto operator==(zstring_view lhs, zstring_view rhs) -> bool { + return std::string_view{lhs} == std::string_view{rhs}; + } + + export constexpr auto split_on(std::string_view s, char c) -> std::pair> { + if (auto i = s.find(c); i != std::string_view::npos) + return std::make_pair(s.substr(0, i), s.substr(i + 1)); + return std::make_pair(s, std::nullopt); + } + + export template + class not_null; + + export template + class not_null { + T* p_; + + struct guaranteed_not_null_t {}; + explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {} + + public: + explicit not_null(T* p) + : p_{p} + { if (!p_) throw std::runtime_error{"not_null constructed with null pointer"}; } + ~not_null() = default; + + not_null(not_null const& other) = default; + not_null(not_null&& other) noexcept = default; + auto operator=(not_null const& rhs) noexcept -> not_null& = default; + auto operator=(not_null&& rhs) noexcept -> not_null& = default; + + friend auto make_not_null(T* p) noexcept -> std::optional { + if (p) return not_null(p, guaranteed_not_null_t{}); + else return std::nullopt; + } + + [[nodiscard]] auto get() const noexcept -> T* { + return p_; + } + + auto operator*() const noexcept -> std::add_lvalue_reference_t { + return *p_; + } + + auto operator->() const noexcept -> T* { + return p_; + } + }; + export template explicit not_null(T*) -> not_null; + + export template<> + class not_null { + lazy_zstring_view s_; + + public: + explicit not_null(lazy_zstring_view s) + : s_{std::move(s)} + { if (!s_) throw std::runtime_error{"not_null constructed with null pointer"}; } + + [[nodiscard]] auto get() const noexcept -> lazy_zstring_view { + return s_; + } + + operator lazy_zstring_view() const noexcept { return s_; } + operator std::string_view() const noexcept { return s_; } + operator char const*() const noexcept { return s_; } + }; + export explicit not_null(lazy_zstring_view s) -> not_null; + +} // namespace routemon::util diff --git a/server/src/xml.cpp b/server/src/xml.cpp new file mode 100644 index 0000000..cad42d1 --- /dev/null +++ b/server/src/xml.cpp @@ -0,0 +1,61 @@ +module; + +#include +#include + +module routemon:xml$impl; + +import :xml; + +namespace routemon::xml { + + auto qname_view::operator==(qname_view const& rhs) const -> bool { + return ns_uri == rhs.ns_uri && local == rhs.local; + } + + executor::executor() : + p_{XML_ParserCreateNS("UTF-8", detail::qname_sep)} + { + XML_SetUserData(p_, this); + XML_SetElementHandler(p_, handle_start_element, handle_end_element); + XML_SetCharacterDataHandler(p_, handle_character_data); + XML_SetProcessingInstructionHandler(p_, handle_processing_instructions); + XML_SetExternalEntityRefHandler(p_, handle_external_entity_ref); + XML_SetNamespaceDeclHandler(p_, handle_start_namespace_decl, handle_end_namespace_decl); + XML_SetXmlDeclHandler(p_, handle_xml_decl); + } + + executor::~executor() { + XML_ParserFree(p_); + } + + auto executor::start() -> void { + continuation_.resume(); + if (ex_) std::rethrow_exception(ex_); + } + + auto executor::read(std::string_view xml, bool is_final) -> void { + if (ex_) + throw std::runtime_error{"refusing to restart parser that was thrown in"}; + // TODO: narrow_cast + if (auto s = XML_Parse(p_, xml.data(), static_cast(xml.size()), is_final); s != XML_STATUS_OK) { + auto errc = XML_GetErrorCode(p_); + if (errc == XML_ERROR_ABORTED) { + assert(ex_); + std::rethrow_exception(ex_); + } else { + throw std::runtime_error{std::format("failed to parse XML: {}", XML_ErrorString(errc))}; + } + } + } + + auto executor::end() -> void { + if (ex_) + throw std::runtime_error{"refusing to restart parser that was thrown in"}; + ev_ = eof_event{}; + advance_ = false; + while (continuation_) continuation_.resume(); + if (ex_) std::rethrow_exception(ex_); + } + +} // namespace routemon::xml diff --git a/server/src/xml.cppm b/server/src/xml.cppm new file mode 100644 index 0000000..957f149 --- /dev/null +++ b/server/src/xml.cppm @@ -0,0 +1,632 @@ +module; + +#include +#include + +export module routemon:xml; + +import std; +import :util; + +// XML parsing module. +// +// Makes heavy use of C++20 coroutines. Provides combinators to build +// XML (data format) parsers with. To keep overhead low (and allow for +// HALO/CoroElide), many non-polymorphic definitions are marked inline +// (which is not the default in module units). This also has the added +// benefit of allowing HALO across TU boundaries, which is practically +// necessary to reduce unnecessary allocations when building parsers +// using the provided combinators. + +// Ensure that Expat is speaking UTF-8 +static_assert(std::is_same_v); + +namespace routemon::xml { + + struct qname_view { + std::string_view ns_uri; + std::string_view local; + + auto operator==(qname_view const& rhs) const -> bool; + }; + + namespace detail { + + constexpr auto qname_sep = '\xFF'; + auto split_name(char const* name) noexcept -> qname_view { + auto [l, r] = util::split_on(name, qname_sep); + if (r) return { .ns_uri = l, .local = *r }; + else return { .ns_uri = std::string_view{}, .local = l }; + } + + } // namespace detail + + class attribute_view_iterator { + std::string_view default_ns_uri_; + char const* const* attrs_; + + auto advance() -> void { + attrs_ += 2; + } + + public: + using difference_type = std::ptrdiff_t; + using value_type = std::pair; + + struct sentinel { + friend constexpr auto operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool { + return !*it.attrs_; + } + }; + + inline explicit attribute_view_iterator(std::string_view default_ns_uri, char const* const* attrs) + : default_ns_uri_{default_ns_uri}, attrs_{attrs} + {} + + inline auto operator*() const -> std::pair { + if (!*attrs_) + throw std::runtime_error{"end of attribute list"}; + auto qname = detail::split_name(attrs_[0]); + if (qname.ns_uri.empty()) + qname.ns_uri = default_ns_uri_; + return std::make_pair(qname, attrs_[1]); + } + + // Pre-increment + inline auto operator++() -> attribute_view_iterator& { + advance(); + return *this; + } + + // Post-increment + inline auto operator++(int) -> attribute_view_iterator { + auto pre = *this; + advance(); + return pre; + } + }; + static_assert(std::input_iterator); + + class attribute_view : std::ranges::view_base { + std::string_view default_ns_uri_; + char const* const* attrs_; + + public: + inline explicit attribute_view(std::string_view default_ns_uri, char const** attrs) + : default_ns_uri_{default_ns_uri}, attrs_{const_cast(attrs)} + {} + + [[nodiscard]] inline auto begin() const -> attribute_view_iterator { + return attribute_view_iterator{default_ns_uri_, attrs_}; + } + + [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel { + return {}; + } + + inline auto lookup(qname_view want) -> std::optional> { + for (auto const& [name, v] : *this) { + if (name == want) { + return util::not_null{util::lazy_zstring_view{v}}; + } + } + return std::nullopt; + } + }; + static_assert(std::ranges::input_range); + + template class promise; + + template + struct [[clang::coro_await_elidable, clang::coro_return_type]] parser { + using promise_type = promise; + using result_type = promise_type::result_type; + using handle_type = std::coroutine_handle; + + private: + handle_type h_; + + public: + explicit parser(handle_type h) + : h_{h} + { assert(h); } + + parser(const parser&) = delete; + parser(parser&& c) noexcept + : h_{std::exchange(c.h_, nullptr)} + {} + auto operator=(const parser&) -> parser& = delete; + auto operator=(parser&&) -> parser& = delete; + + [[nodiscard]] auto promise() const -> promise_type& { + return h_.promise(); + } + + ~parser() { + if (h_) h_.destroy(); + } + }; + + struct start_element_event { + qname_view name; + attribute_view attrs; + }; + struct end_element_event { + qname_view name; + }; + struct character_data_event { + std::string_view data; + }; + struct processing_instructions_event { + util::lazy_zstring_view target; + util::lazy_zstring_view data; + }; + struct xml_decl_event { + util::lazy_zstring_view version; + util::lazy_zstring_view encoding; + std::optional standalone; + }; + struct eof_event {}; + using event = std::variant; + template + concept event_type = requires(event ev) { std::get(ev); }; + + class executor; + using executor_ref = util::not_null; + + struct current_event_t { + executor_ref executor; + }; + auto current_event(executor_ref executor) -> current_event_t { + return current_event_t{executor}; + } + + class promise_base { + executor_ref executor_; + std::coroutine_handle continuation_ = nullptr; + + public: + // Not having this constructor marked inline messes with coroutine + // HALO. (Hours 'wasted': many) + inline explicit promise_base(executor_ref executor) + : executor_{executor} + {} + + [[nodiscard]] inline auto executor() const -> executor& { + return *executor_; + } + + inline auto base_handle() -> std::coroutine_handle { + return std::coroutine_handle::from_promise(*this); + } + + [[nodiscard]] inline auto continuation() const -> std::coroutine_handle { + return continuation_; + } + inline auto set_continuation(std::coroutine_handle c) -> void { + continuation_ = c; + } + }; + + struct position { + std::size_t line; + std::size_t col; + }; + + class executor { + XML_Parser p_; + std::exception_ptr ex_ = nullptr; + std::coroutine_handle continuation_ = nullptr; + std::vector> default_namespace_; + std::unordered_map> namespaces_; + std::optional ev_; + bool advance_ = true; + + inline auto try_handle_event(event ev) noexcept -> void { + assert(!ex_); + + try { + ev_ = std::move(ev); + } catch (...) { + ex_ = std::current_exception(); + return; + } + advance_ = false; + if (!continuation_) { + // Parser returned (all subparsers are done) and has set the continuation to nullptr. + if (auto s = XML_StopParser(p_, /* resumable */ false); s != XML_STATUS_OK) { + ex_ = std::make_exception_ptr(std::runtime_error{"unexpected error when stopping XML parser"}); + return; + } + ex_ = std::make_exception_ptr(std::runtime_error{"parser did not consume entire XML document"}); + return; + } + continuation_.resume(); + if (ex_) { + // Not sure if it's useful to report this error. + std::ignore = XML_StopParser(p_, /* resumable */ false); + } + } + + static auto handle_start_element(void* ctx, char const* name, char const** attrs) noexcept -> void { + auto qname = detail::split_name(name); + static_cast(ctx)->try_handle_event(start_element_event{ + .name = qname, + .attrs = attribute_view{qname.ns_uri, attrs}, + }); + } + static auto handle_end_element(void* ctx, char const* name) noexcept -> void { + static_cast(ctx)->try_handle_event(end_element_event{ + .name = detail::split_name(name), + }); + } + static auto handle_character_data(void* ctx, char const* s, int len) noexcept -> void { + static_cast(ctx)->try_handle_event(character_data_event{ + .data = std::string_view{s, static_cast(len)}, + }); + } + static auto handle_processing_instructions(void* ctx, char const* target, char const* data) noexcept -> void { + static_cast(ctx)->try_handle_event(processing_instructions_event{ + .target = util::lazy_zstring_view{target}, + .data = util::lazy_zstring_view{data}, + }); + } + static auto handle_external_entity_ref(XML_Parser, char const* /* context */, char const* /* base */, char const* /* system_id */, char const* /* public_id */) noexcept -> int { + return XML_STATUS_ERROR; + } + static auto handle_start_namespace_decl(void* ctx, char const* prefix, char const* uri) noexcept -> void { + if (prefix) { + static_cast(ctx)->namespaces_[std::string_view{prefix}].emplace_back(uri); + } else { + static_cast(ctx)->default_namespace_.push_back(uri ? std::make_optional(uri) : std::nullopt); + } + } + static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept -> void { + if (prefix) { + static_cast(ctx)->namespaces_[std::string_view{prefix}].pop_back(); + } else { + static_cast(ctx)->default_namespace_.pop_back(); + } + } + static auto handle_xml_decl(void * ctx, char const* version, char const* encoding, int standalone) noexcept -> void { + static_cast(ctx)->try_handle_event(xml_decl_event{ + .version = util::lazy_zstring_view{version}, + .encoding = util::lazy_zstring_view{encoding}, + .standalone = standalone < 0 ? std::nullopt : std::make_optional(standalone > 0), + }); + } + + inline auto advance_flag() -> bool { + return advance_; + } + inline auto set_exception(std::exception_ptr ex) -> void { + ex_ = std::move(ex); + } + [[nodiscard]] inline auto take_exception() -> std::exception_ptr { + return std::exchange(ex_, nullptr); + } + + template friend class promise; + + public: + executor(); + + executor(executor const&) = delete; + executor(executor&&) = delete; + auto operator=(executor const&) -> executor& = delete; + auto operator=(executor&&) -> executor& = delete; + + ~executor(); + + inline auto set_continuation(std::coroutine_handle c) -> void { + continuation_ = c; + } + inline auto set_advance_flag() -> void { + if (!ev_ || !std::holds_alternative(ev_.value())) { + advance_ = true; + } + } + inline auto event() const -> std::optional const& { + return ev_; + } + inline auto resolve_namespace(std::string_view prefix) -> std::optional { + if (auto it = namespaces_.find(prefix); it != namespaces_.end() && !it->second.empty()) + return it->second.back(); + return std::nullopt; + } + [[nodiscard]] inline auto position() -> position { + return { + .line = XML_GetCurrentLineNumber(p_), + .col = XML_GetCurrentColumnNumber(p_), + }; + } + + auto start() -> void; + auto read(std::string_view xml, bool is_final) -> void; + auto end() -> void; + }; + + template + class promise_returnable : public promise_base { + std::optional returned_value_; + + public: + using result_type = T; + using promise_base::promise_base; + + template + auto return_value(U&& v) -> void { + returned_value_.emplace(std::forward(v)); + } + auto returned_value() -> T&& { + if (!returned_value_) + throw std::runtime_error{"XML coroutine did not return"}; + return std::forward(returned_value_.value()); + } + }; + + template<> + class promise_returnable : public promise_base { + public: + using result_type = void; + using promise_base::promise_base; + + auto return_void() -> void {} + }; + + template + class promise : public promise_returnable { + public: + // Called with all the coroutine's arguments. + // Ignoring all but the first argument, which should be the executor. + template + explicit promise(executor_ref executor, Args&&...) + : promise_returnable{executor} + {} + + auto handle() -> std::coroutine_handle> { + return {parser::handle_type::from_promise(*this)}; + } + + auto get_return_object() -> parser { + return parser{handle()}; + } + + auto initial_suspend() { + return std::suspend_always{}; + } + auto final_suspend() noexcept { + struct awaiter { + std::coroutine_handle<> h_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } + auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> { return h_; } + constexpr auto await_resume() const noexcept -> void { return; } + }; + if (this->continuation()) { + return awaiter{this->continuation()}; + } else { + this->executor().set_continuation(nullptr); + return awaiter{std::noop_coroutine()}; + } + } + + auto unhandled_exception() -> void { + this->executor().set_exception(std::current_exception()); + } + + auto await_transform(current_event_t const& req) { + struct awaiter { + executor_ref executor_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { + return !executor_->advance_flag(); + } + auto await_suspend(std::coroutine_handle> h) -> void { + executor_->set_continuation(h.promise().base_handle()); + } + [[nodiscard]] auto await_resume() const -> event { + assert(executor_->event()); + return executor_->event().value(); + } + }; + return awaiter{req.executor}; + } + + template + auto await_transform(parser const& coro) { + struct [[clang::coro_await_elidable]] awaiter { + util::not_null*> next_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } + auto await_suspend(std::coroutine_handle> h) -> std::coroutine_handle<> { + // Passed coroutine handle will be the same as parser::handle_type::from_promise(*this) + next_->set_continuation(h.promise().base_handle()); + return next_->handle(); + } + auto await_resume() -> U { + // Promise is still valid since coroutine frame is still alive (and suspended): + // control was transferred back to this coroutine via symmetric transfer in + // final_suspend(). Assuming that the destructor for coro still needs to run. + if (auto ex = next_->executor().take_exception()) { + std::rethrow_exception(ex); + } else { + if constexpr (!std::is_void_v) { + return std::move(next_->returned_value()); + } + } + } + }; + return awaiter{util::not_null{&coro.promise()}}; + } + }; + + // Helpers for handling XML documents. Non-polymorphic functions + // should be marked inline to allow HALO across TU boundaries. + + template + concept unconstrained = true; + + template concept C> + concept parser_of = requires { + typename T::result_type; + requires std::same_as, T>; + requires C; + }; + + template T> + using parser_result_t = T::result_type; + + template + concept parser_invocable = std::invocable && parser_of, unconstrained>; + + template + requires parser_invocable + using parser_invoke_result_t = parser_result_t>; + + template auto expect_event(executor_ref e) -> parser { + auto ev = co_await current_event(e); + if (!std::holds_alternative(ev)) { + auto pos = e->position(); + throw std::runtime_error{std::format("at {}:{}: unexpected event type, have {}", pos.line, pos.col, ev.index())}; + } + e->set_advance_flag(); + co_return std::get(ev); + } + + inline auto expect_start_element(executor_ref e, qname_view want) -> parser { + auto ev = co_await expect_event(e); + if (ev.name != want) { + auto pos = e->position(); + throw std::runtime_error{std::format("at {}:{}: unexpected element started", pos.line, pos.col)}; + } + co_return ev.attrs; + } + + inline auto allow_start_element(executor_ref e, qname_view want) -> parser> { + auto ev = co_await current_event(e); + if (auto const* pev = std::get_if(&ev)) { + if (pev->name == want) { + e->set_advance_flag(); + co_return pev->attrs; + } + } + co_return std::nullopt; + } + + inline auto expect_end_element(executor_ref e, qname_view want) -> parser { + auto ev = co_await expect_event(e); + if (ev.name != want) { + throw std::runtime_error{"unexpected element ended"}; + } + } + + inline auto ignore_whitespace(executor_ref e) -> parser { + auto all_whitespace = [](std::string_view s) -> bool { + for (auto c : s) + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') + return false; + return true; + }; + + while (true) { + auto ev = co_await current_event(e); + if (auto const* pev = std::get_if(&ev); pev && all_whitespace(pev->data)) { + e->set_advance_flag(); + } else { + co_return; + } + } + } + + auto expect_element(executor_ref e, qname_view want, parser_invocable auto p) + -> parser> + { + co_await ignore_whitespace(e); + auto attrs = co_await expect_start_element(e, want); + auto&& res = co_await p(e, attrs); + co_await expect_end_element(e, want); + co_await ignore_whitespace(e); + co_return std::forward>(res); + } + + auto allow_element(executor_ref e, qname_view want, parser_invocable auto p) + -> parser>> + { + co_await ignore_whitespace(e); + if (auto mattrs = co_await allow_start_element(e, want)) { + auto&& res = co_await p(e, *mattrs); + co_await expect_end_element(e, want); + co_await ignore_whitespace(e); + co_return std::make_optional(std::forward>(res)); + } + co_return std::nullopt; + } + + auto allow_element(executor_ref e, qname_view want, parser_invocable auto p) + -> parser + requires std::is_void_v> + { + co_await ignore_whitespace(e); + if (auto mattrs = co_await allow_start_element(e, want)) { + co_await p(e, *mattrs); + co_await expect_end_element(e, want); + co_await ignore_whitespace(e); + co_return true; + } + co_return false; + } + + inline auto ignore_contents(executor_ref e, std::optional muntil = std::nullopt) -> parser { + std::size_t depth = 0; + while (true) { + auto ev = co_await current_event(e); + if (auto* pev = std::get_if(&ev)) { + if (depth == 0 && muntil && pev->name == *muntil) { + co_return; + } else { + depth++; + } + } else if (std::holds_alternative(ev)) { + if (depth == 0) { + co_return; + } else { + depth--; + } + } + e->set_advance_flag(); + } + } + inline auto ignore_element_contents(executor_ref e, attribute_view) -> parser { + co_await ignore_contents(e); + } + + inline auto read_string(executor_ref e) -> parser { + std::string s; + while (true) { + auto ev = co_await current_event(e); + if (auto* pev = std::get_if(&ev)) { + e->set_advance_flag(); + s += pev->data; + } else { + co_return s; + } + } + } + inline auto read_string_contents(executor_ref e, attribute_view) -> parser { + co_return co_await read_string(e); + } + + template + auto hohalo() { + return [](Args&&... args) -> std::invoke_result_t { + co_return co_await f(std::forward(args)...); + }; + } + +} // namespace routemon::xml diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f927d39 --- /dev/null +++ b/web/index.html @@ -0,0 +1,52 @@ + + + + + + + + + +
+
+ +
+
+ + +
+
+ +
+
+ +
+ + +

+

+
+ Meer informatie + +
+ +
+ + + + + diff --git a/web/script.js b/web/script.js new file mode 100644 index 0000000..b3179b3 --- /dev/null +++ b/web/script.js @@ -0,0 +1,149 @@ +const dialog = document.querySelector("dialog"); +const closeButton = document.querySelector("dialog button"); +closeButton.addEventListener("click", () => { + dialog.close(); +}); + +let map = L.map("map"); + +L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + attribution: '© OpenStreetMap contributors', +}).addTo(map); + +let layerGroup = L.layerGroup().addTo(map); + +document.forms["gpx-upload-form"].addEventListener("submit", (event) => { + event.preventDefault(); + const gpxFileInput = document.getElementById("gpx-file-input"); + if (gpxFileInput.files.length !== 1) { + alert("Voor de upload moet er exact één GPX-bestand zijn geselecteerd"); + return; + } + load(gpxFileInput.files[0]); +}); + +function el(name, attrs, ...children) { + const node = document.createElement(name); + for (const [k, v] of Object.entries(attrs)) { + node.setAttribute(k, v); + } + node.replaceChildren(...children); + return node; +} + +function txt(s) { + return document.createTextNode(s); +} + +function tbl(rowcols) { + return el("table", {}, + el("tbody", {}, + ...rowcols.map((cols) => + el("tr", {}, + ...cols.map((col) => el("td", {}, col)), + )))); +} + +async function showProblem(rsp) { + const problem = await rsp.json(); + + document.getElementById("dialog-title").innerText = problem.title; + if (problem.detail) + document.getElementById("dialog-message").innerText = problem.detail; + else + document.getElementById("dialog-message").innerText = ""; + dialog.showModal(); + + let rows = [ + [txt("HTTP-statuscode"), + el("a", { "href": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/" + rsp.status }, + txt(rsp.status + " (" + rsp.statusText + ")"))], + [txt("Probleemtype"), + el("code", {}, txt(problem.type))], + [txt("Trace-ID"), + el("code", {}, txt(rsp.headers.get("X-Routemon-Trace-Id")))], + ]; + if (problem.instance) { + rows.push([txt("Instantie"), + el("code", {}, txt(problem.instance))]); + } + document.getElementById("dialog-more-info").replaceChildren(tbl(rows)); + + return; +} + +async function getSysinfo() { + const rsp = await fetch("http://localhost:8284/sysinfo", { + method: "GET", + headers: { + "Accept-Language": "nl-NL", + }, + }); + if (!rsp.ok) { + await showProblem(rsp); + return; + } + + const res = await rsp.json(); + document.getElementById("situation-publication-of").innerText = new Date(res.using_publication_of).toLocaleString(); +} + +async function load(gpxFile) { + const rsp = await fetch("http://localhost:8284/gpx", { + method: "POST", + headers: { + "Accept-Language": "nl-NL", + }, + body: gpxFile, + }); + if (!rsp.ok) { + await showProblem(rsp); + return; + } + + const res = await rsp.json(); + layerGroup.clearLayers(); + + let bounds = null; + for (const track of res.tracks) { + for (const segment of track.segments) { + const polyline = L.polyline(segment.points, { color: "blue" }).addTo(layerGroup); + if (bounds === null) { + bounds = polyline.getBounds(); + } else { + bounds.extend(polyline.getBounds()); + } + } + } + if (bounds !== null) { + map.fitBounds(bounds); + } + + res.relevant_situations.forEach((sit) => { + let firstComment = sit.comments[0] || ""; + const lfIndex = firstComment.indexOf("\n"); + if (lfIndex !== -1) { + firstComment = firstComment.substring(0, lfIndex); + } + const commentEl = + el("div", {}, + el("code", {}, txt(sit.id)), + txt(": " + firstComment)); + + const markerLayer = L.marker(sit.location); + markerLayer.addTo(layerGroup).bindPopup(commentEl); + + sit.relevant_road_closures.forEach((rc) => { + rc.relevant_lss.forEach((ls) => { + const lineLayer = L.polyline(ls, { + color: "purple", + dashArray: "5, 10", + dashOffset: "0", + }); + lineLayer.addTo(layerGroup); + }); + }); + }); +} + +getSysinfo(); diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..a96354c --- /dev/null +++ b/web/style.css @@ -0,0 +1,62 @@ +html, body { + margin: 0px; + height: 100%; + font-family: sans-serif; + font-size: 14px; +} + +#map { height: 100%; } + +.box { + padding: 10px; + background-color: white; + border: 2px solid #b0b0b0; + border-radius: 4px; +} + +dialog button { + float: right; +} + +details summary { + user-select: none; + cursor: pointer; +} + +table { + border-collapse: collapse; + border: 1px solid black; + margin: 5px 0px; +} + +th, td { + border: 1px solid black; + padding: 5px 5px; +} + +form[name="gpx-upload-form"] { + z-index: 1000; + display: flex; + gap: 16pt; + position: absolute; + top: 10px; + right: 10px; +} + +#sysinfo { + z-index: 1000; + display: flex; + flex-direction: column; + gap: 5px; + position: absolute; + bottom: 10px; + left: 10px; +} + +#sysinfo h3 { + margin: 0px 0px; +} + +.help-cursor { + cursor: help; +} -- cgit v1.3
+

Routemon

+
Broncode +
+ + Systeeminformatie + + + + + + +
Situatiepublicatie van<onbekend>
+
+