diff options
53 files changed, 3458 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore index a27208efa..1223f074f 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,5 @@ src/include/taler_dbevents.h src/bank-lib/taler-exchange-wire-gateway-client src/exchange/taler-exchange-drain src/kyclogic/taler-exchange-kyc-tester +src/auditor/exchange-httpd-drain.err +src/templating/libmustach.a diff --git a/configure.ac b/configure.ac index bbb0fabff..290afd0af 100644 --- a/configure.ac +++ b/configure.ac @@ -541,6 +541,7 @@ AC_CONFIG_FILES([Makefile src/mhd/Makefile src/pq/Makefile src/sq/Makefile + src/templating/Makefile src/util/Makefile ]) AC_OUTPUT diff --git a/contrib/gana b/contrib/gana -Subproject e340b038e3a5ae3fbb87a68b534bd2646df5e6f +Subproject ce901edbaf496244f50f45b221d0c2c929c4763 diff --git a/contrib/uncrustify_precommit b/contrib/uncrustify_precommit index 24873330f..78ca1be39 100755 --- a/contrib/uncrustify_precommit +++ b/contrib/uncrustify_precommit @@ -1,7 +1,7 @@ #!/bin/sh # use as .git/hooks/pre-commit - +exit 0 exec 1>&2 RET=0 diff --git a/src/Makefile.am b/src/Makefile.am index 05c0b7421..8e398106e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -22,6 +22,7 @@ SUBDIRS = \ curl \ $(PQ_DIR) \ $(SQ_DIR) \ + templating \ mhd \ bank-lib \ exchangedb \ diff --git a/src/include/Makefile.am b/src/include/Makefile.am index 5cb1698cc..a535986cb 100644 --- a/src/include/Makefile.am +++ b/src/include/Makefile.am @@ -26,6 +26,7 @@ talerinclude_HEADERS = \ taler_pq_lib.h \ taler_signatures.h \ taler_sq_lib.h \ + taler_templating_lib.h \ taler_twister_testing_lib.h EXTRA_DIST = \ diff --git a/src/include/taler_templating_lib.h b/src/include/taler_templating_lib.h new file mode 100644 index 000000000..bad200f50 --- /dev/null +++ b/src/include/taler_templating_lib.h @@ -0,0 +1,65 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler_templating_lib.h + * @brief logic to load and complete HTML templates + * @author Christian Grothoff + */ +#ifndef TALER_TEMPLATING_LIB_H +#define TALER_TEMPLATING_LIB_H + +#include <microhttpd.h> + + +/** + * Load a @a template and substitute using @a root, returning + * the result to the @a connection with the given + * @a http_status code. + * + * @param connection the connection we act upon + * @param http_status code to use on success + * @param template basename of the template to load + * @param instance_id instance ID, used to compute static files URL + * @param taler_uri value for "Taler:" header to set, or NULL + * @param root JSON object to pass as the root context + * @return #GNUNET_OK on success (reply queued), #GNUNET_NO if an error was queued, + * #GNUNET_SYSERR on failure (to queue an error) + */ +enum GNUNET_GenericReturnValue +TALER_TEMPLATING_reply (struct MHD_Connection *connection, + unsigned int http_status, + const char *template, + const char *instance_id, + const char *taler_uri, + json_t *root); + +/** + * Preload templates. + * + * @param subsystem name of the subsystem, "merchant" or "exchange" + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TALER_TEMPLATING_init (const char *subsystem); + + +/** + * Nicely shut down templating subsystem. + */ +void +TALER_TEMPLATING_done (void); + +#endif diff --git a/src/kyclogic/taler-exchange-kyc-tester.c b/src/kyclogic/taler-exchange-kyc-tester.c index 9133b68c9..9d75b3ec8 100644 --- a/src/kyclogic/taler-exchange-kyc-tester.c +++ b/src/kyclogic/taler-exchange-kyc-tester.c @@ -494,6 +494,7 @@ clean_kwh (struct TEKT_RequestContext *rc) * @param provider_section * @param provider_legitimization_id legi to look up * @param[out] h_payto where to write the result + * @param[out] legi_row where to write the row ID for the legitimization ID * @return database transaction status */ static enum GNUNET_DB_QueryStatus @@ -501,13 +502,15 @@ kyc_provider_account_lookup ( void *cls, const char *provider_section, const char *provider_legitimization_id, - struct TALER_PaytoHashP *h_payto) + struct TALER_PaytoHashP *h_payto, + uint64_t *legi_row) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Simulated account lookup using `%s/%s'\n", provider_section, provider_legitimization_id); *h_payto = cmd_line_h_payto; + *legi_row = kyc_row_id; return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } diff --git a/src/templating/.gitignore b/src/templating/.gitignore new file mode 100644 index 000000000..b2bf6ef99 --- /dev/null +++ b/src/templating/.gitignore @@ -0,0 +1 @@ +test_mustach_jansson diff --git a/src/templating/AUTHORS b/src/templating/AUTHORS new file mode 100644 index 000000000..2fcc60437 --- /dev/null +++ b/src/templating/AUTHORS @@ -0,0 +1,23 @@ +Main author: + José Bollo <jobol@nonadev.net> + +Contributors: + Abhishek Mishra + Atlas + Harold L Marzan + Lailton Fernando Mariano + Sami Kerola + Sijmen J. Mulder + Tomasz Sieprawski + +Packagers: + pkgsrc: Sijmen J. Mulder + alpine linux: Lucas Ramage + +Thanks to issue submitters: + Dante Torres + @fabbe + Johann Oskarsson + Mark Bucciarelli + Paul Wisehart + Thierry Fournier diff --git a/src/templating/LICENSE-2.0.txt b/src/templating/LICENSE-2.0.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/src/templating/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/templating/Makefile.am b/src/templating/Makefile.am new file mode 100644 index 000000000..e0aef033c --- /dev/null +++ b/src/templating/Makefile.am @@ -0,0 +1,50 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include $(LIBGCRYPT_CFLAGS) + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + + +lib_LTLIBRARIES = \ + libtalertemplating.la + +noinst_LTLIBRARIES = \ + libmustach.la + +libtalertemplating_la_SOURCES = \ + mustach.c mustach.h \ + mustach-jansson.c mustach-jansson.h \ + templating_api.c +libtalertemplating_la_LIBADD = \ + -ltalermhd \ + -ltalerutil \ + -lgnunetutil \ + $(XLIB) +libtalertemplating_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined + + +libmustach_la_SOURCES = \ + mustach.c mustach.h \ + mustach-jansson.c mustach-jansson.h + +test_mustach_jansson_SOURCES = \ + test_mustach_jansson.c +test_mustach_jansson_LDADD = \ + -lgnunetutil \ + -lmustach.la \ + $(XLIB) + +check_PROGRAMS = \ + test_mustach_jansson + +check_SCRIPTS = \ + run-original-tests.sh + +TESTS = $(check_SCRIPTS) $(check_PROGRAMS) + +EXTRA_DIST = \ + $(check_SCRIPTS) diff --git a/src/templating/ORIGIN b/src/templating/ORIGIN new file mode 100644 index 000000000..fafb0ae79 --- /dev/null +++ b/src/templating/ORIGIN @@ -0,0 +1,9 @@ +Cloned originally from https://gitlab.com/jobol/mustach/ + +Changes: +======== + +Renamed original Makefile to Makefile.orig and wrote Makefile.am for us. + +Added run-original-tests.sh shell script as a wrapper around Makefile.org +to us the original build process for the test suite. diff --git a/src/templating/README.md b/src/templating/README.md new file mode 100644 index 000000000..a6df19f64 --- /dev/null +++ b/src/templating/README.md @@ -0,0 +1,214 @@ +# Introduction to Mustach 0.99 + +`mustach` is a C implementation of the [mustache](http://mustache.github.io "main site for mustache") +template specification. + +The main site for `mustach` is on [gitlab](https://gitlab.com/jobol/mustach). + +The best way to use mustach is to copy the files **mustach.h** and **mustach.c** +directly into your project and use it. + +Alternatively, make and meson files are provided for building `mustach` and +`libmustach.so` shared library. + +## Distributions offering mustach package + +### Alpine Linux + +```sh +apk add mustach +apk add mustach-lib +apk add mustach-dev +``` + +### NetBSD + +```sh +cd devel/mustach +make +``` + +See http://pkgsrc.se/devel/mustach + +## Using Mustach from sources + +The file **mustach.h** is the main documentation. Look at it. + +The current source files are: + +- **mustach.c** core implementation of mustache in C +- **mustach.h** header file for core definitions +- **mustach-json-c.c** tiny json wrapper of mustach using [json-c](https://github.com/json-c/json-c) +- **mustach-json-c.h** header file for using the tiny JSON wrapper +- **mustach-tool.c** simple tool for applying template files to a JSON file + +The file **mustach-json-c.c** is the main example of use of **mustach** core +and it is also a practical implementation that can be used. It uses the library +json-c. (NOTE for Mac OS: available through homebrew). + +HELP REQUESTED TO GIVE EXAMPLE BASED ON OTHER LIBRARIES (ex: janson, ...). + +The tool **mustach** is build using `make`, its usage is: + + mustach json template [template]... + +It then outputs the result of applying the templates files to the JSON file. + +### Portability + +Some system does not provide *open_memstream*. In that case, tell your +preferred compiler to declare the preprocessor symbol **NO_OPEN_MEMSTREAM**. +Example: + + gcc -DNO_OPEN_MEMSTREAM + +### Integration + +The file **mustach.h** is the main documentation. Look at it. + +The file **mustach-json-c.c** provides a good example of integration. + +If you intend to use basic HTML/XML escaping and standard C FILE, the callbacks +of the interface **mustach_itf** that you have to implement are: +`enter`, `next`, `leave`, `get`. + +If you intend to use specific escaping and/or specific output, the callbacks +of the interface **mustach_itf** that you have to implement are: +`enter`, `next`, `leave`, `get` and `emit`. + +### Extensions + +By default, the current implementation provides the following extensions: + +#### Explicit Substitution + +This is a core extension implemented in file **mustach.c**. + +In somecases the name of the key used for substitution begins with a +character reserved for mustach: one of `#`, `^`, `/`, `&`, `{`, `>` and `=`. +This extension introduces the special character `:` to explicitly +tell mustach to just substitute the value. So `:` becomes a new special +character. + +#### Value Testing and Comparing + +This are a tool extension implemented in file **mustach-json-c.c**. + +These extensions allows you to test the value of the selected key. +They allow to write `key=value` (matching test) or `key=!value` +(not matching test) in any query. + +The specific comparison extension also allows to compare if greater, +lesser, etc.. than a value. It allows to write `key>value`. + +It the comparator sign appears in the first column it is ignored +as if it was escaped. + +#### Access to current value + +The value of the current field can be accessed using single dot like +in `{{#key}}{{.}}{{/key}}` that applied to `{"key":3.14}` produces `3.14` +and `{{#array}} {{.}}{{/array}}` applied to `{"array":[1,2]}` produces +` 1 2`. + +#### Iteration on objects + +Using the pattern `{{#X.*}}...{{/X.*}}` it is possible to iterate on +fields of `X`. Example: `{{s.*}} {{*}}:{{.}}{{/s.*}}` applied on +`{"s":{"a":1,"b":true}}` produces ` a:1 b:true`. Here the single star +`{{*}}` is replaced by the iterated key and the single dot `{{.}}` is +replaced by its value. + +### Removing Extensions + +When compiling mustach.c or mustach-json-c.c, +extensions can be removed by defining macros +using option -D. + +The possible macros are of 3 categories, the global, +the mustach core specific and the mustach-json-c example +of implementation specific. + +#### Global macros + +- `NO_EXTENSION_FOR_MUSTACH` + + This macro disables any current or future + extensions for the core or the example. + +#### Macros for the core mustach engine (mustach.c) + +- `NO_COLON_EXTENSION_FOR_MUSTACH` + + This macro remove the ability to use colon (:) + as explicit command for variable substitution. + This extension allows to have name starting + with one of the mustach character `:#^/&{=>` + +- `NO_ALLOW_EMPTY_TAG` + + Generate the error MUSTACH_ERROR_EMPTY_TAG automatically + when an empty tag is encountered. + +#### Macros for the implementation example (mustach-json-c.c) + +- `NO_EQUAL_VALUE_EXTENSION_FOR_MUSTACH` + + This macro allows the program to check whether + the actual value is equal to an expected value. + This is useful in `{{#key=val}}` or `{{^key=val}}` + with the corresponding `{{/key=val}}`. + It can also be used in `{{key=val}}` but this + doesn't seem to be useful. + +- `NO_COMPARE_VALUE_EXTENSION_FOR_MUSTACH` + + This macro allows the program to compare the actual + value with an expected value. The comparison operators + are `=`, `>`, `<`, `>=`, `<=`. The meaning of the + comparison numeric or alphabetic depends on the type + of the inspected value. Also the result of the comparison + can be inverted if the value starts with `!`. + Example of use: `{{key>=val}}`, or `{{#key>=val}}` and + `{{^key>=val}}` with their matching `{{/key>=val}}`. + +- `NO_USE_VALUE_ESCAPE_FIRST_EXTENSION_FOR_MUSTACH` + + This macro fordids automatic escaping of coparison + sign appearing at first column. + +- `NO_JSON_POINTER_EXTENSION_FOR_MUSTACH` + + This macro removes the possible use of JSON pointers. + JSON pointers are defined in IETF RFC 6901. + If not set, any key starting with "/" is a JSON pointer. + This implies to use the colon to introduce keys. + So `NO_COLON_EXTENSION_FOR_MUSTACH` implies + `NO_JSON_POINTER_EXTENSION_FOR_MUSTACH`. + A special escaping is used for `=`, `<`, `>` signs when + values comparisons are enabled: `~=` gives `=` in the key. + +- `NO_OBJECT_ITERATION_FOR_MUSTACH` + + Disable the object iteration extension. That extension allows + to iterate over the keys of an object. The iteration on object + is selected by using the selector `{{#key.*}}`. In the context + of iterating over object keys, the single key `{{*}}` returns the + key and `{{.}}` returns the value. + +- `NO_SINGLE_DOT_EXTENSION_FOR_MUSTACH` + + Disable access to current object value using single dot + like in `{{.}}`. + +- `NO_INCLUDE_PARTIAL_FALLBACK` + + Disable include of file by partial pattern like `{{> name}}`. + By default if a such pattern is found, **mustach** search + for `name` in the current json context. This what is done + historically and when `NO_INCLUDE_PARTIAL_FALLBACK` is defined. + When `NO_INCLUDE_PARTIAL_FALLBACK` is defined, if the value is + found in the json context, the files `name` and `name.mustache` + are searched in that order and the first file found is used + as partial content. The macro `INCLUDE_PARTIAL_EXTENSION` can + be use for changing the extension added. diff --git a/src/templating/meson.build b/src/templating/meson.build new file mode 100644 index 000000000..c7ecc8dfc --- /dev/null +++ b/src/templating/meson.build @@ -0,0 +1,12 @@ +project('mustach', 'c', + version: '1.0.0' +) + +mustach_inc = include_directories('.') +mustach_lib = shared_library('mustach', + 'mustach.c', + include_directories: mustach_inc +) + +mustach_dep = declare_dependency(link_with: mustach_lib, + include_directories: mustach_inc) diff --git a/src/templating/mustach-jansson.c b/src/templating/mustach-jansson.c new file mode 100644 index 000000000..2aed58291 --- /dev/null +++ b/src/templating/mustach-jansson.c @@ -0,0 +1,417 @@ +/* + Copyright (C) 2020 Taler Systems SA + + Original license: + Author: José Bollo <jobol@nonadev.net> + Author: José Bollo <jose.bollo@iot.bzh> + + https://gitlab.com/jobol/mustach + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "platform.h" +#include "mustach-jansson.h" + +struct Context +{ + /** + * Context object. + */ + json_t *cont; + + /** + * Current object. + */ + json_t *obj; + + /** + * Opaque object iterator. + */ + void *iter; + + /** + * Current index when iterating over an array. + */ + unsigned int index; + + /** + * Count when iterating over an array. + */ + unsigned int count; + + bool is_objiter; +}; + +enum Bang +{ + BANG_NONE, + BANG_I18N, + BANG_STRINGIFY, + BANG_AMOUNT_CURRENCY, + BANG_AMOUNT_DECIMAL, +}; + +struct JanssonClosure +{ + json_t *root; + mustach_jansson_write_cb writecb; + int depth; + + /** + * Did the last find(..) call result in an iterable? + */ + struct Context stack[MUSTACH_MAX_DEPTH]; + + /** + * The last object we found should be iterated over. + */ + bool found_iter; + + /** + * Last bang we found. + */ + enum Bang found_bang; + + /** + * Language for i18n lookups. + */ + const char *lang; +}; + + +static json_t * +walk (json_t *obj, const char *path) +{ + char *saveptr = NULL; + char *sp = GNUNET_strdup (path); + char *p = sp; + while (true) + { + char *tok = strtok_r (p, ".", &saveptr); + if (tok == NULL) + break; + obj = json_object_get (obj, tok); + if (obj == NULL) + break; + p = NULL; + } + GNUNET_free (sp); + return obj; +} + + +static json_t * +find (struct JanssonClosure *e, const char *name) +{ + json_t *obj = NULL; + char *path = GNUNET_strdup (name); + char *bang; + + bang = strchr (path, '!'); + + e->found_bang = BANG_NONE; + + if (NULL != bang) + { + *bang = 0; + bang++; + + if (0 == strcmp (bang, "i18n")) + e->found_bang = BANG_I18N; + else if (0 == strcmp(bang, "stringify")) + e->found_bang = BANG_STRINGIFY; + else if (0 == strcmp(bang, "amount_decimal")) + e->found_bang = BANG_AMOUNT_CURRENCY; + else if (0 == strcmp(bang, "amount_currency")) + e->found_bang = BANG_AMOUNT_DECIMAL; + } + + if (BANG_I18N == e->found_bang && NULL != e->lang) + { + char *aug_path; + GNUNET_asprintf (&aug_path, "%s_i18n.%s", path, e->lang); + obj = walk (e->stack[e->depth].obj, aug_path); + GNUNET_free (aug_path); + } + + if (NULL == obj) + { + obj = walk (e->stack[e->depth].obj, path); + } + + GNUNET_free (path); + + return obj; +} + + +static int +start(void *closure) +{ + struct JanssonClosure *e = closure; + e->depth = 0; + e->stack[0].cont = NULL; + e->stack[0].obj = e->root; + e->stack[0].index = 0; + e->stack[0].count = 1; + e->lang = json_string_value (json_object_get (e->root, "$language")); + return MUSTACH_OK; +} + + +static int +emituw (void *closure, const char *buffer, size_t size, int escape, FILE *file) +{ + struct JanssonClosure *e = closure; + if (!escape) + e->writecb (file, buffer, size); + else + do + { + switch (*buffer) + { + case '<': + e->writecb (file, "<", 4); + break; + case '>': + e->writecb (file, ">", 4); + break; + case '&': + e->writecb (file, "&", 5); + break; + default: + e->writecb (file, buffer, 1); + break; + } + buffer++; + } + while(--size); + return MUSTACH_OK; +} + + +static int +enter(void *closure, const char *name) +{ + struct JanssonClosure *e = closure; + json_t *o = find(e, name); + if (++e->depth >= MUSTACH_MAX_DEPTH) + return MUSTACH_ERROR_TOO_DEEP; + + if (json_is_object (o)) + { + if (e->found_iter) + { + void *iter = json_object_iter (o); + if (NULL == iter) + { + e->depth--; + return 0; + } + e->stack[e->depth].is_objiter = 1; + e->stack[e->depth].iter = iter; + e->stack[e->depth].obj = json_object_iter_value (iter); + e->stack[e->depth].cont = o; + } + else + { + e->stack[e->depth].is_objiter = 0; + e->stack[e->depth].obj = o; + e->stack[e->depth].cont = o; + } + return 1; + } + + if (json_is_array (o)) + { + unsigned int size = json_array_size (o); + if (size == 0) + { + e->depth--; + return 0; + } + e->stack[e->depth].count = size; + e->stack[e->depth].cont = o; + e->stack[e->depth].obj = json_array_get (o, 0); + e->stack[e->depth].index = 0; + e->stack[e->depth].is_objiter = 0; + return 1; + } + + e->depth--; + return 0; +} + + +static int +next (void *closure) +{ + struct JanssonClosure *e = closure; + struct Context *ctx; + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + ctx = &e->stack[e->depth]; + if (ctx->is_objiter) + { + ctx->iter = json_object_iter_next (ctx->obj, ctx->iter); + if (NULL == ctx->iter) + return 0; + ctx->obj = json_object_iter_value (ctx->iter); + return 1; + } + ctx->index++; + if (ctx->index >= ctx->count) + return 0; + ctx->obj = json_array_get (ctx->cont, ctx->index); + return 1; +} + +static int +leave (void *closure) +{ + struct JanssonClosure *e = closure; + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + e->depth--; + return 0; +} + +static void +freecb (void *v) +{ + free (v); +} + +static int +get (void *closure, const char *name, struct mustach_sbuf *sbuf) +{ + struct JanssonClosure *e = closure; + json_t *obj; + + if ( (0 == strcmp (name, "*") ) && + (e->stack[e->depth].is_objiter ) ) + { + sbuf->value = json_object_iter_key (e->stack[e->depth].iter); + return MUSTACH_OK; + } + obj = find (e, name); + if (NULL != obj) + { + switch (e->found_bang) + { + case BANG_I18N: + case BANG_NONE: + { + const char *s = json_string_value (obj); + if (NULL != s) + { + sbuf->value = s; + return MUSTACH_OK; + } + } + break; + case BANG_STRINGIFY: + sbuf->value = json_dumps (obj, JSON_INDENT (2)); + sbuf->freecb = freecb; + return MUSTACH_OK; + case BANG_AMOUNT_DECIMAL: + { + char *s; + char *c; + if (!json_is_string (obj)) + break; + s = strdup (json_string_value (obj)); + c = strchr (s, ':'); + if (NULL != c) + *c = 0; + sbuf->value = s; + sbuf->freecb = freecb; + return MUSTACH_OK; + } + break; + case BANG_AMOUNT_CURRENCY: + { + const char *s; + if (!json_is_string (obj)) + break; + s = json_string_value (obj); + s = strchr (s, ':'); + if (NULL == s) + break; + sbuf->value = s + 1; + return MUSTACH_OK; + } + break; + default: + break; + } + } + sbuf->value = ""; + return MUSTACH_OK; +} + +static struct mustach_itf itf = { + .start = start, + .put = NULL, + .enter = enter, + .next = next, + .leave = leave, + .partial =NULL, + .get = get, + .emit = NULL, + .stop = NULL +}; + +static struct mustach_itf itfuw = { + .start = start, + .put = NULL, + .enter = enter, + .next = next, + .leave = leave, + .partial = NULL, + .get = get, + .emit = emituw, + .stop = NULL +}; + +int fmustach_jansson (const char *template, json_t *root, FILE *file) +{ + struct JanssonClosure e = { 0 }; + e.root = root; + return fmustach(template, &itf, &e, file); +} + +int fdmustach_jansson (const char *template, json_t *root, int fd) +{ + struct JanssonClosure e = { 0 }; + e.root = root; + return fdmustach(template, &itf, &e, fd); +} + +int mustach_jansson (const char *template, json_t *root, char **result, size_t *size) +{ + struct JanssonClosure e = { 0 }; + e.root = root; + e.writecb = NULL; + return mustach(template, &itf, &e, result, size); +} + +int umustach_jansson (const char *template, json_t *root, mustach_jansson_write_cb writecb, void *closure) +{ + struct JanssonClosure e = { 0 }; + e.root = root; + e.writecb = writecb; + return fmustach(template, &itfuw, &e, closure); +} + diff --git a/src/templating/mustach-jansson.h b/src/templating/mustach-jansson.h new file mode 100644 index 000000000..27dcdd64f --- /dev/null +++ b/src/templating/mustach-jansson.h @@ -0,0 +1,82 @@ +/* + Copyright (C) 2020 Taler Systems SA + + Original license: + Author: José Bollo <jose.bollo@iot.bzh> + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef _mustach_jansson_h_included_ +#define _mustach_jansson_h_included_ + +#include <taler/taler_json_lib.h> +#include "mustach.h" + +/** + * fmustach_jansson - Renders the mustache 'template' in 'file' for 'root'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * \@file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int fmustach_jansson(const char *template, json_t *root, FILE *file); + +/** + * fmustach_jansson - Renders the mustache 'template' in 'fd' for 'root'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int fdmustach_jansson(const char *template, json_t *root, int fd); + + +/** + * fmustach_jansson - Renders the mustache 'template' in 'result' for 'root'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_jansson(const char *template, json_t *root, char **result, size_t *size); + +/** + * umustach_jansson - Renders the mustache 'template' for 'root' to custom writer 'writecb' with 'closure'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * @writecb: the function that write values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +typedef int (*mustach_jansson_write_cb)(void *closure, const char *buffer, size_t size); +extern int umustach_jansson(const char *template, json_t *root, mustach_jansson_write_cb writecb, void *closure); + +#endif + diff --git a/src/templating/mustach-tool.c b/src/templating/mustach-tool.c new file mode 100644 index 000000000..364e34a84 --- /dev/null +++ b/src/templating/mustach-tool.c @@ -0,0 +1,155 @@ +/* + Author: José Bollo <jobol@nonadev.net> + Author: José Bollo <jose.bollo@iot.bzh> + + https://gitlab.com/jobol/mustach + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#define _GNU_SOURCE + +#include <stdlib.h> +#include <stdio.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> +#include <libgen.h> + +#include "mustach-json-c.h" + +static const size_t BLOCKSIZE = 8192; + +static const char *errors[] = { + "??? unreferenced ???", + "system", + "unexpected end", + "empty tag", + "tag too long", + "bad separators", + "too depth", + "closing", + "bad unescape tag", + "invalid interface", + "item not found", + "partial not found" +}; + +static void help(char *prog) +{ + printf("usage: %s json-file mustach-templates...\n", basename(prog)); + exit(0); +} + +static char *readfile(const char *filename) +{ + int f; + struct stat s; + char *result; + size_t size, pos; + ssize_t rc; + + result = NULL; + if (filename[0] == '-' && filename[1] == 0) + f = dup(0); + else + f = open(filename, O_RDONLY); + if (f < 0) { + fprintf(stderr, "Can't open file: %s\n", filename); + exit(1); + } + + fstat(f, &s); + switch (s.st_mode & S_IFMT) { + case S_IFREG: + size = s.st_size; + break; + case S_IFSOCK: + case S_IFIFO: + size = BLOCKSIZE; + break; + default: + fprintf(stderr, "Bad file: %s\n", filename); + exit(1); + } + + pos = 0; + result = malloc(size + 1); + do { + if (result == NULL) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + rc = read(f, &result[pos], (size - pos) + 1); + if (rc < 0) { + fprintf(stderr, "Error while reading %s\n", filename); + exit(1); + } + if (rc > 0) { + pos += (size_t)rc; + if (pos > size) { + size = pos + BLOCKSIZE; + result = realloc(result, size + 1); + } + } + } while(rc > 0); + + close(f); + result[pos] = 0; + return result; +} + +int main(int ac, char **av) +{ + struct json_object *o; + char *t; + char *prog = *av; + int s; + + (void)ac; /* unused */ + + if (*++av) { + if (!strcmp(*av, "-h") || !strcmp(*av, "--help")) + help(prog); + if (av[0][0] == '-' && !av[0][1]) + o = json_object_from_fd(0); + else + o = json_object_from_file(av[0]); +#if JSON_C_VERSION_NUM >= 0x000D00 + if (json_util_get_last_err() != NULL) { + fprintf(stderr, "Bad json: %s (file %s)\n", json_util_get_last_err(), av[0]); + exit(1); + } + else +#endif + if (o == NULL) { + fprintf(stderr, "Aborted: null json (file %s)\n", av[0]); + exit(1); + } + while(*++av) { + t = readfile(*av); + s = fmustach_json_c(t, o, stdout); + if (s != 0) { + s = -s; + if (s < 1 || s >= (int)(sizeof errors / sizeof * errors)) + s = 0; + fprintf(stderr, "Template error %s (file %s)\n", errors[s], *av); + } + free(t); + } + json_object_put(o); + } + return 0; +} + diff --git a/src/templating/mustach.c b/src/templating/mustach.c new file mode 100644 index 000000000..caa80dcc9 --- /dev/null +++ b/src/templating/mustach.c @@ -0,0 +1,472 @@ +/* + Author: José Bollo <jobol@nonadev.net> + Author: José Bollo <jose.bollo@iot.bzh> + + https://gitlab.com/jobol/mustach + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#define _GNU_SOURCE + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <ctype.h> +#ifdef _WIN32 +#include <malloc.h> +#endif +#ifdef __sun +# include <alloca.h> +#endif + +#include "mustach.h" + +#if defined(NO_EXTENSION_FOR_MUSTACH) +# undef NO_COLON_EXTENSION_FOR_MUSTACH +# define NO_COLON_EXTENSION_FOR_MUSTACH +# undef NO_ALLOW_EMPTY_TAG +# define NO_ALLOW_EMPTY_TAG +#endif + +struct iwrap { + int (*emit)(void *closure, const char *buffer, size_t size, int escape, FILE *file); + void *closure; /* closure for: enter, next, leave, emit, get */ + int (*put)(void *closure, const char *name, int escape, FILE *file); + void *closure_put; /* closure for put */ + int (*enter)(void *closure, const char *name); + int (*next)(void *closure); + int (*leave)(void *closure); + int (*get)(void *closure, const char *name, struct mustach_sbuf *sbuf); + int (*partial)(void *closure, const char *name, struct mustach_sbuf *sbuf); + void *closure_partial; /* closure for partial */ +}; + +#if !defined(NO_OPEN_MEMSTREAM) +static FILE *memfile_open(char **buffer, size_t *size) +{ + return open_memstream(buffer, size); +} +static void memfile_abort(FILE *file, char **buffer, size_t *size) +{ + fclose(file); + free(*buffer); + *buffer = NULL; + *size = 0; +} +static int memfile_close(FILE *file, char **buffer, size_t *size) +{ + int rc; + + /* adds terminating null */ + rc = fputc(0, file) ? MUSTACH_ERROR_SYSTEM : 0; + fclose(file); + if (rc == 0) + /* removes terminating null of the length */ + (*size)--; + else { + free(*buffer); + *buffer = NULL; + *size = 0; + } + return rc; +} +#else +static FILE *memfile_open(char **buffer, size_t *size) +{ + /* + * We can't provide *buffer and *size as open_memstream does but + * at least clear them so the caller won't get bad data. + */ + *buffer = NULL; + *size = 0; + + return tmpfile(); +} +static void memfile_abort(FILE *file, char **buffer, size_t *size) +{ + fclose(file); + *buffer = NULL; + *size = 0; +} +static int memfile_close(FILE *file, char **buffer, size_t *size) +{ + int rc; + size_t s; + char *b; + + s = (size_t)ftell(file); + b = malloc(s + 1); + if (b == NULL) { + rc = MUSTACH_ERROR_SYSTEM; + errno = ENOMEM; + s = 0; + } else { + rewind(file); + if (1 == fread(b, s, 1, file)) { + rc = 0; + b[s] = 0; + } else { + rc = MUSTACH_ERROR_SYSTEM; + free(b); + b = NULL; + s = 0; + } + } + *buffer = b; + *size = s; + return rc; +} +#endif + +static inline void sbuf_reset(struct mustach_sbuf *sbuf) +{ + sbuf->value = NULL; + sbuf->freecb = NULL; + sbuf->closure = NULL; +} + +static inline void sbuf_release(struct mustach_sbuf *sbuf) +{ + if (sbuf->releasecb) + sbuf->releasecb(sbuf->value, sbuf->closure); +} + +static int iwrap_emit(void *closure, const char *buffer, size_t size, int escape, FILE *file) +{ + size_t i, j; + + (void)closure; /* unused */ + + if (!escape) + return fwrite(buffer, size, 1, file) != 1 ? MUSTACH_ERROR_SYSTEM : MUSTACH_OK; + + i = 0; + while (i < size) { + j = i; + while (j < size && buffer[j] != '<' && buffer[j] != '>' && buffer[j] != '&') + j++; + if (j != i && fwrite(&buffer[i], j - i, 1, file) != 1) + return MUSTACH_ERROR_SYSTEM; + if (j < size) { + switch(buffer[j++]) { + case '<': + if (fwrite("<", 4, 1, file) != 1) + return MUSTACH_ERROR_SYSTEM; + break; + case '>': + if (fwrite(">", 4, 1, file) != 1) + return MUSTACH_ERROR_SYSTEM; + break; + case '&': + if (fwrite("&", 5, 1, file) != 1) + return MUSTACH_ERROR_SYSTEM; + break; + default: break; + } + } + i = j; + } + return MUSTACH_OK; +} + +static int iwrap_put(void *closure, const char *name, int escape, FILE *file) +{ + struct iwrap *iwrap = closure; + int rc; + struct mustach_sbuf sbuf; + size_t length; + + sbuf_reset(&sbuf); + rc = iwrap->get(iwrap->closure, name, &sbuf); + if (rc >= 0) { + length = strlen(sbuf.value); + if (length) + rc = iwrap->emit(iwrap->closure, sbuf.value, length, escape, file); + sbuf_release(&sbuf); + } + return rc; +} + +static int iwrap_partial(void *closure, const char *name, struct mustach_sbuf *sbuf) +{ + struct iwrap *iwrap = closure; + int rc; + FILE *file; + size_t size; + char *result; + + result = NULL; + file = memfile_open(&result, &size); + if (file == NULL) + rc = MUSTACH_ERROR_SYSTEM; + else { + rc = iwrap->put(iwrap->closure_put, name, 0, file); + if (rc < 0) + memfile_abort(file, &result, &size); + else { + rc = memfile_close(file, &result, &size); + if (rc == 0) { + sbuf->value = result; + sbuf->freecb = free; + } + } + } + return rc; +} + +static int process(const char *template, struct iwrap *iwrap, FILE *file, const char *opstr, const char *clstr) +{ + struct mustach_sbuf sbuf; + char name[MUSTACH_MAX_LENGTH + 1], c, *tmp; + const char *beg, *term; + struct { const char *name, *again; size_t length; int enabled, entered; } stack[MUSTACH_MAX_DEPTH]; + size_t oplen, cllen, len, l; + int depth, rc, enabled; + + enabled = 1; + oplen = strlen(opstr); + cllen = strlen(clstr); + depth = 0; + for(;;) { + beg = strstr(template, opstr); + if (beg == NULL) { + /* no more mustach */ + if (enabled && template[0]) { + rc = iwrap->emit(iwrap->closure, template, strlen(template), 0, file); + if (rc < 0) + return rc; + } + return depth ? MUSTACH_ERROR_UNEXPECTED_END : MUSTACH_OK; + } + if (enabled && beg != template) { + rc = iwrap->emit(iwrap->closure, template, (size_t)(beg - template), 0, file); + if (rc < 0) + return rc; + } + beg += oplen; + term = strstr(beg, clstr); + if (term == NULL) + return MUSTACH_ERROR_UNEXPECTED_END; + template = term + cllen; + len = (size_t)(term - beg); + c = *beg; + switch(c) { + case '!': + case '=': + break; + case '{': + for (l = 0 ; clstr[l] == '}' ; l++); + if (clstr[l]) { + if (!len || beg[len-1] != '}') + return MUSTACH_ERROR_BAD_UNESCAPE_TAG; + len--; + } else { + if (term[l] != '}') + return MUSTACH_ERROR_BAD_UNESCAPE_TAG; + template++; + } + c = '&'; + /*@fallthrough@*/ + case '^': + case '#': + case '/': + case '&': + case '>': +#if !defined(NO_COLON_EXTENSION_FOR_MUSTACH) + case ':': +#endif + beg++; len--; + default: + while (len && isspace(beg[0])) { beg++; len--; } + while (len && isspace(beg[len-1])) len--; +#if !defined(NO_ALLOW_EMPTY_TAG) + if (len == 0) + return MUSTACH_ERROR_EMPTY_TAG; +#endif + if (len > MUSTACH_MAX_LENGTH) + return MUSTACH_ERROR_TAG_TOO_LONG; + memcpy(name, beg, len); + name[len] = 0; + break; + } + switch(c) { + case '!': + /* comment */ + /* nothing to do */ + break; + case '=': + /* defines separators */ + if (len < 5 || beg[len - 1] != '=') + return MUSTACH_ERROR_BAD_SEPARATORS; + beg++; + len -= 2; + for (l = 0; l < len && !isspace(beg[l]) ; l++); + if (l == len) + return MUSTACH_ERROR_BAD_SEPARATORS; + oplen = l; + tmp = alloca(oplen + 1); + memcpy(tmp, beg, oplen); + tmp[oplen] = 0; + opstr = tmp; + while (l < len && isspace(beg[l])) l++; + if (l == len) + return MUSTACH_ERROR_BAD_SEPARATORS; + cllen = len - l; + tmp = alloca(cllen + 1); + memcpy(tmp, beg + l, cllen); + tmp[cllen] = 0; + clstr = tmp; + break; + case '^': + case '#': + /* begin section */ + if (depth == MUSTACH_MAX_DEPTH) + return MUSTACH_ERROR_TOO_DEEP; + rc = enabled; + if (rc) { + rc = iwrap->enter(iwrap->closure, name); + if (rc < 0) + return rc; + } + stack[depth].name = beg; + stack[depth].again = template; + stack[depth].length = len; + stack[depth].enabled = enabled; + stack[depth].entered = rc; + if ((c == '#') == (rc == 0)) + enabled = 0; + depth++; + break; + case '/': + /* end section */ + if (depth-- == 0 || len != stack[depth].length || memcmp(stack[depth].name, name, len)) + return MUSTACH_ERROR_CLOSING; + rc = enabled && stack[depth].entered ? iwrap->next(iwrap->closure) : 0; + if (rc < 0) + return rc; + if (rc) { + template = stack[depth++].again; + } else { + enabled = stack[depth].enabled; + if (enabled && stack[depth].entered) + iwrap->leave(iwrap->closure); + } + break; + case '>': + /* partials */ + if (enabled) { + sbuf_reset(&sbuf); + rc = iwrap->partial(iwrap->closure_partial, name, &sbuf); + if (rc >= 0) { + rc = process(sbuf.value, iwrap, file, opstr, clstr); + sbuf_release(&sbuf); + } + if (rc < 0) + return rc; + } + break; + default: + /* replacement */ + if (enabled) { + rc = iwrap->put(iwrap->closure_put, name, c != '&', file); + if (rc < 0) + return rc; + } + break; + } + } +} + +int fmustach(const char *template, struct mustach_itf *itf, void *closure, FILE *file) +{ + int rc; + struct iwrap iwrap; + + /* check validity */ + if (!itf->enter || !itf->next || !itf->leave || (!itf->put && !itf->get)) + return MUSTACH_ERROR_INVALID_ITF; + + /* init wrap structure */ + iwrap.closure = closure; + if (itf->put) { + iwrap.put = itf->put; + iwrap.closure_put = closure; + } else { + iwrap.put = iwrap_put; + iwrap.closure_put = &iwrap; + } + if (itf->partial) { + iwrap.partial = itf->partial; + iwrap.closure_partial = closure; + } else if (itf->get) { + iwrap.partial = itf->get; + iwrap.closure_partial = closure; + } else { + iwrap.partial = iwrap_partial; + iwrap.closure_partial = &iwrap; + } + iwrap.emit = itf->emit ? itf->emit : iwrap_emit; + iwrap.enter = itf->enter; + iwrap.next = itf->next; + iwrap.leave = itf->leave; + iwrap.get = itf->get; + + /* process */ + rc = itf->start ? itf->start(closure) : 0; + if (rc == 0) + rc = process(template, &iwrap, file, "{{", "}}"); + if (itf->stop) + itf->stop(closure, rc); + return rc; +} + +int fdmustach(const char *template, struct mustach_itf *itf, void *closure, int fd) +{ + int rc; + FILE *file; + + file = fdopen(fd, "w"); + if (file == NULL) { + rc = MUSTACH_ERROR_SYSTEM; + errno = ENOMEM; + } else { + rc = fmustach(template, itf, closure, file); + fclose(file); + } + return rc; +} + +int mustach(const char *template, struct mustach_itf *itf, void *closure, char **result, size_t *size) +{ + int rc; + FILE *file; + size_t s; + + *result = NULL; + if (size == NULL) + size = &s; + file = memfile_open(result, size); + if (file == NULL) + rc = MUSTACH_ERROR_SYSTEM; + else { + rc = fmustach(template, itf, closure, file); + if (rc < 0) + memfile_abort(file, result, size); + else + rc = memfile_close(file, result, size); + } + return rc; +} + diff --git a/src/templating/mustach.h b/src/templating/mustach.h new file mode 100644 index 000000000..ad952275c --- /dev/null +++ b/src/templating/mustach.h @@ -0,0 +1,241 @@ +/* + Author: José Bollo <jobol@nonadev.net> + Author: José Bollo <jose.bollo@iot.bzh> + + https://gitlab.com/jobol/mustach + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef _mustach_h_included_ +#define _mustach_h_included_ + +struct mustach_sbuf; /* see below */ + +/** + * Current version of mustach and its derivates + */ +#define MUSTACH_VERSION 99 +#define MUSTACH_VERSION_MAJOR (MUSTACH_VERSION / 100) +#define MUSTACH_VERSION_MINOR (MUSTACH_VERSION % 100) + +/** + * Maximum nested imbrications supported + */ +#define MUSTACH_MAX_DEPTH 256 + +/** + * Maximum length of tags in mustaches {{...}} + */ +#define MUSTACH_MAX_LENGTH 1024 + +/** + * mustach_itf - interface for callbacks + * + * All of this function should return a negative value to stop + * the mustache processing. The returned negative value will be + * then returned to the caller of mustach as is. + * + * The functions enter and next should return 0 or 1. + * + * All other functions should normally return MUSTACH_OK (zero). + * If it returns a negative value, it means an error that stop + * the process and that is reported to the caller. + * + * @start: If defined (can be NULL), starts the mustach processing + * of the closure, called at the very beginning before any + * mustach processing occurs. + * + * @put: If defined (can be NULL), writes the value of 'name' + * to 'file' with 'escape' or not. + * As an extension (see NO_ALLOW_EMPTY_TAG), the 'name' can be + * the empty string. In that later case an implementation can + * return MUSTACH_ERROR_EMPTY_TAG to refuse empty names. + * If NULL and 'get' NULL the error MUSTACH_ERROR_INVALID_ITF + * is returned. + * + * @enter: Enters the section of 'name' if possible. + * Musts return 1 if entered or 0 if not entered. + * When 1 is returned, the function 'leave' will always be called. + * Conversely 'leave' is never called when enter returns 0 or + * a negative value. + * When 1 is returned, the function must activate the first + * item of the section. + * + * @next: Activates the next item of the section if it exists. + * Musts return 1 when the next item is activated. + * Musts return 0 when there is no item to activate. + * + * @leave: Leaves the last entered section + * + * @partial: If defined (can be NULL), returns in 'sbuf' the content of the + * partial of 'name'. @see mustach_sbuf + * If NULL but 'get' not NULL, 'get' is used instead of partial. + * If NULL and 'get' NULL and 'put' not NULL, 'put' is called with + * a true FILE. + * + * @emit: If defined (can be NULL), writes the 'buffer' of 'size' with 'escape'. + * If NULL the standard function 'fwrite' is used with a true FILE. + * If not NULL that function is called instead of 'fwrite' to output + * text. + * It implies that if you define either 'partial' or 'get' callback, + * the meaning of 'FILE *file' is abstract for mustach's process and + * then you can use 'FILE*file' pass any kind of pointer (including NULL) + * to the function 'fmustach'. An example of a such behaviour is given by + * the implementation of 'umustach_json_c'. + * + * @get: If defined (can be NULL), returns in 'sbuf' the value of 'name'. + * As an extension (see NO_ALLOW_EMPTY_TAG), the 'name' can be + * the empty string. In that later case an implementation can + * return MUSTACH_ERROR_EMPTY_TAG to refuse empty names. + * If NULL and 'put' NULL the error MUSTACH_ERROR_INVALID_ITF + * is returned. + * + * @stop: If defined (can be NULL), stops the mustach processing + * of the closure, called at the very end after all mustach + * processing occurerd. The status returned by the processing + * is passed to the stop. + * + * The array below summarize status of callbacks: + * + * FULLY OPTIONAL: start partial + * MANDATORY: enter next leave + * COMBINATORIAL: put emit get + * + * Not definig a MANDATORY callback returns error MUSTACH_ERROR_INVALID_ITF. + * + * For COMBINATORIAL callbacks the array below summarize possible combinations: + * + * combination : put : emit : get : abstract FILE + * -------------+---------+---------+---------+----------------------- + * HISTORIC : defined : NULL : NULL : NO: standard FILE + * MINIMAL : NULL : NULL : defined : NO: standard FILE + * CUSTOM : NULL : defined : defined : YES: abstract FILE + * DUCK : defined : NULL : defined : NO: standard FILE + * DANGEROUS : defined : defined : any : YES or NO, depends on 'partial' + * INVALID : NULL : any : NULL : - + * + * The DUCK case runs on one leg. 'get' is not used if 'partial' is defined + * but is used for 'partial' if 'partial' is NULL. Thus for clarity, do not use + * it that way but define 'partial' and let 'get' NULL. + * + * The DANGEROUS case is special: it allows abstract FILE if 'partial' is defined + * but forbids abstract FILE when 'partial' is NULL. + * + * The INVALID case returns error MUSTACH_ERROR_INVALID_ITF. + */ +struct mustach_itf { + int (*start)(void *closure); + int (*put)(void *closure, const char *name, int escape, FILE *file); + int (*enter)(void *closure, const char *name); + int (*next)(void *closure); + int (*leave)(void *closure); + int (*partial)(void *closure, const char *name, struct mustach_sbuf *sbuf); + int (*emit)(void *closure, const char *buffer, size_t size, int escape, FILE *file); + int (*get)(void *closure, const char *name, struct mustach_sbuf *sbuf); + void (*stop)(void *closure, int status); +}; + +/** + * mustach_sbuf - Interface for handling zero terminated strings + * + * That structure is used for returning zero terminated strings -in 'value'- + * to mustach. The callee can provide a function for releasing the returned + * 'value'. Three methods for releasing the string are possible. + * + * 1. no release: set either 'freecb' or 'releasecb' with NULL (done by default) + * 2. release without closure: set 'freecb' to its expected value + * 3. release with closure: set 'releasecb' and 'closure' to their expected values + * + * @value: The value of the string. That value is not changed by mustach -const-. + * + * @freecb: The function to call for freeing the value without closure. + * For convenience, signature of that callback is compatible with 'free'. + * Can be NULL. + * + * @releasecb: The function to release with closure. + * Can be NULL. + * + * @closure: The closure to use for 'releasecb'. + */ +struct mustach_sbuf { + const char *value; + union { + void (*freecb)(void*); + void (*releasecb)(const char *value, void *closure); + }; + void *closure; +}; + +/* + * Definition of error codes returned by mustach + */ +#define MUSTACH_OK 0 +#define MUSTACH_ERROR_SYSTEM -1 +#define MUSTACH_ERROR_UNEXPECTED_END -2 +#define MUSTACH_ERROR_EMPTY_TAG -3 +#define MUSTACH_ERROR_TAG_TOO_LONG -4 +#define MUSTACH_ERROR_BAD_SEPARATORS -5 +#define MUSTACH_ERROR_TOO_DEEP -6 +#define MUSTACH_ERROR_CLOSING -7 +#define MUSTACH_ERROR_BAD_UNESCAPE_TAG -8 +#define MUSTACH_ERROR_INVALID_ITF -9 +#define MUSTACH_ERROR_ITEM_NOT_FOUND -10 +#define MUSTACH_ERROR_PARTIAL_NOT_FOUND -11 + +/* You can use definition below for user specific error */ +#define MUSTACH_ERROR_USER_BASE -100 +#define MUSTACH_ERROR_USER(x) (MUSTACH_ERROR_USER_BASE-(x)) + +/** + * fmustach - Renders the mustache 'template' in 'file' for 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * \@file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int fmustach(const char *template, struct mustach_itf *itf, void *closure, FILE *file); + +/** + * fmustach - Renders the mustache 'template' in 'fd' for 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int fdmustach(const char *template, struct mustach_itf *itf, void *closure, int fd); + +/** + * fmustach - Renders the mustache 'template' in 'result' for 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach(const char *template, struct mustach_itf *itf, void *closure, char **result, size_t *size); + +#endif + diff --git a/src/templating/run-original-tests.sh b/src/templating/run-original-tests.sh new file mode 100755 index 000000000..9c7d34cdd --- /dev/null +++ b/src/templating/run-original-tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -eu +# The build fails if libjson-c-dev is not installed. +# That's OK, we don't otherwise need it and don't +# even bother testing for it in configure.ac. +# However, in that case, skip the test suite. + +make -f Makefile.orig mustach || exit 77 +make -f Makefile.orig test +make -f Makefile.orig clean || true diff --git a/src/templating/templating_api.c b/src/templating/templating_api.c new file mode 100644 index 000000000..6d5c7c791 --- /dev/null +++ b/src/templating/templating_api.c @@ -0,0 +1,449 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file templating_api.c + * @brief logic to load and complete HTML templates + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "taler_util.h" +#include "taler_mhd_lib.h" +#include "taler_templating_lib.h" +#include "mustach.h" +#include "mustach-jansson.h" +#include <gnunet/gnunet_mhd_compat.h> + + +/** + * Entry in a key-value array we use to cache templates. + */ +struct TVE +{ + /** + * A name, used as the key. NULL for the last entry. + */ + char *name; + + /** + * Language the template is in. + */ + char *lang; + + /** + * 0-terminated (!) file data to return for @e name and @e lang. + */ + char *value; + +}; + + +/** + * Array of templates loaded into RAM. + */ +static struct TVE *loaded; + +/** + * Length of the #loaded array. + */ +static unsigned int loaded_length; + + +/** + * Load Mustach template into memory. Note that we intentionally cache + * failures, that is if we ever failed to load a template, we will never try + * again. + * + * @param connection the connection we act upon + * @param name name of the template file to load + * (MUST be a 'static' string in memory!) + * @return NULL on error, otherwise the template + */ +static const char * +lookup_template (struct MHD_Connection *connection, + const char *name) +{ + struct TVE *best = NULL; + const char *lang; + + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + /* find best match by language */ + for (unsigned int i = 0; i<loaded_length; i++) + { + if (0 != strcmp (loaded[i].name, + name)) + continue; /* does not match by name */ + if ( (NULL == best) || + (TALER_language_matches (lang, + loaded[i].lang) > + TALER_language_matches (lang, + best->lang) ) ) + best = &loaded[i]; + } + if (NULL == best) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No templates found in `%s'\n", + name); + return NULL; + } + return best->value; +} + + +/** + * Get the base URL for static resources. + * + * @param con the MHD connection + * @param instance_id the instance ID + * @returns the static files base URL, guaranteed + * to have a trailing slash. + */ +static char * +make_static_url (struct MHD_Connection *con, + const char *instance_id) +{ + const char *host; + const char *forwarded_host; + const char *uri_path; + struct GNUNET_Buffer buf = { 0 }; + + host = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + "Host"); + forwarded_host = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + "X-Forwarded-Host"); + + uri_path = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + "X-Forwarded-Prefix"); + if (NULL != forwarded_host) + host = forwarded_host; + + if (NULL == host) + { + GNUNET_break (0); + return NULL; + } + + GNUNET_assert (NULL != instance_id); + + if (GNUNET_NO == TALER_mhd_is_https (con)) + GNUNET_buffer_write_str (&buf, + "http://"); + else + GNUNET_buffer_write_str (&buf, + "https://"); + GNUNET_buffer_write_str (&buf, + host); + if (NULL != uri_path) + GNUNET_buffer_write_path (&buf, + uri_path); + if (0 != strcmp ("default", + instance_id)) + { + GNUNET_buffer_write_path (&buf, + "instances"); + GNUNET_buffer_write_path (&buf, + instance_id); + } + GNUNET_buffer_write_path (&buf, + "static/"); + return GNUNET_buffer_reap_str (&buf); +} + + +/** + * Load a @a template and substitute using @a root, returning + * the result to the @a connection with the given + * @a http_status code. + * + * @param connection the connection we act upon + * @param http_status code to use on success + * @param template basename of the template to load + * @param instance_id instance ID, used to compute static files URL + * @param taler_uri value for "Taler:" header to set, or NULL + * @param root JSON object to pass as the root context + * @return #GNUNET_OK on success (reply queued), #GNUNET_NO if an error was queued, + * #GNUNET_SYSERR on failure (to queue an error) + */ +enum GNUNET_GenericReturnValue +TALER_TEMPLATING_reply (struct MHD_Connection *connection, + unsigned int http_status, + const char *template, + const char *instance_id, + const char *taler_uri, + json_t *root) +{ + struct MHD_Response *reply; + char *body; + size_t body_size; + + { + const char *tmpl; + int eno; + + tmpl = lookup_template (connection, + template); + if (NULL == tmpl) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load template `%s'\n", + template); + if (MHD_YES != + TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_ACCEPTABLE, + TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE, + template)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + /* Add default values to the context */ + { + char *static_url = make_static_url (connection, + instance_id); + json_object_set (root, + "static_url", + json_string (static_url)); + GNUNET_free (static_url); + } + if (0 != + (eno = mustach_jansson (tmpl, + root, + &body, + &body_size))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "mustach failed on template `%s' with error %d\n", + template, + eno); + if (MHD_YES != + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE, + template)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + } + + /* try to compress reply if client allows it */ + { + bool compressed = false; + + if (MHD_YES == + TALER_MHD_can_compress (connection)) + { + compressed = TALER_MHD_body_compress ((void **) &body, + &body_size); + } + reply = MHD_create_response_from_buffer (body_size, + body, + MHD_RESPMEM_MUST_FREE); + if (NULL == reply) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (compressed) + { + if (MHD_NO == + MHD_add_response_header (reply, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) + { + GNUNET_break (0); + MHD_destroy_response (reply); + return GNUNET_SYSERR; + } + } + } + + /* Add standard headers */ + if (NULL != taler_uri) + GNUNET_break (MHD_NO != + MHD_add_response_header (reply, + "Taler", + taler_uri)); + GNUNET_break (MHD_NO != + MHD_add_response_header (reply, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/html")); + + /* Actually return reply */ + { + MHD_RESULT ret; + + ret = MHD_queue_response (connection, + http_status, + reply); + MHD_destroy_response (reply); + if (MHD_NO == ret) + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Function called with a template's filename. + * + * @param cls closure + * @param filename complete filename (absolute path) + * @return #GNUNET_OK to continue to iterate, + * #GNUNET_NO to stop iteration with no error, + * #GNUNET_SYSERR to abort iteration with error! + */ +static enum GNUNET_GenericReturnValue +load_template (void *cls, + const char *filename) +{ + char *lang; + char *end; + int fd; + struct stat sb; + char *map; + const char *name; + + if ('.' == filename[0]) + return GNUNET_OK; + + name = strrchr (filename, + '/'); + if (NULL == name) + name = filename; + else + name++; + lang = strchr (name, + '.'); + if (NULL == lang) + return GNUNET_OK; /* name must include .$LANG */ + lang++; + end = strchr (lang, + '.'); + if ( (NULL == end) || + (0 != strcmp (end, + ".must")) ) + return GNUNET_OK; /* name must end with '.must' */ + + /* finally open template */ + fd = open (filename, + O_RDONLY); + if (-1 == fd) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + filename); + + return GNUNET_SYSERR; + } + if (0 != + fstat (fd, + &sb)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + filename); + GNUNET_break (0 == close (fd)); + return GNUNET_OK; + } + map = GNUNET_malloc_large (sb.st_size + 1); + if (NULL == map) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "malloc"); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + if (sb.st_size != + read (fd, + map, + sb.st_size)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "read", + filename); + GNUNET_break (0 == close (fd)); + return GNUNET_OK; + } + GNUNET_break (0 == close (fd)); + GNUNET_array_grow (loaded, + loaded_length, + loaded_length + 1); + loaded[loaded_length - 1].name = GNUNET_strndup (name, + (lang - 1) - name); + loaded[loaded_length - 1].lang = GNUNET_strndup (lang, + end - lang); + loaded[loaded_length - 1].value = map; + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_TEMPLATING_init (const char *subsystem) +{ + char *dn; + int ret; + + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_asprintf (&dn, + "%s/%s/templates/", + subsystem, + path); + GNUNET_free (path); + } + ret = GNUNET_DISK_directory_scan (dn, + &load_template, + NULL); + GNUNET_free (dn); + if (-1 == ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +void +TALER_TEMPLATING_done (void) +{ + for (unsigned int i = 0; i<loaded_length; i++) + { + GNUNET_free (loaded[i].name); + GNUNET_free (loaded[i].lang); + GNUNET_free (loaded[i].value); + } + GNUNET_array_grow (loaded, + loaded_length, + 0); +} + + +/* end of templating_api.c */ diff --git a/src/templating/test1/.gitignore b/src/templating/test1/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test1/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test1/json b/src/templating/test1/json new file mode 100644 index 000000000..5b2e3d83a --- /dev/null +++ b/src/templating/test1/json @@ -0,0 +1,23 @@ +{ + "name": "Chris", + "value": 10000, + "taxed_value": 6000, + "in_ca": true, + "person": false, + "repo": [ + { "name": "resque", "who": [ { "committer": "joe" }, { "reviewer": "avrel" }, { "committer": "william" } ] }, + { "name": "hub", "who": [ { "committer": "jack" }, { "reviewer": "avrel" }, { "committer": "greg" } ] }, + { "name": "rip", "who": [ { "reviewer": "joe" }, { "reviewer": "jack" }, { "committer": "greg" } ] } + ], + "person?": { "name": "Jon" }, + "special": "----{{extra}}----", + "extra": 3.14159, + "#sharp": "#", + "!bang": "!", + "/slash": "/", + "^circ": "^", + "=equal": "=", + ":colon": ":", + ">greater": ">", + "~tilde": "~" +} diff --git a/src/templating/test1/must b/src/templating/test1/must new file mode 100644 index 000000000..723f966c4 --- /dev/null +++ b/src/templating/test1/must @@ -0,0 +1,43 @@ +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +Shown. +{{#person}} + Never shown! +{{/person}} +{{^person}} + No person +{{/person}} + +{{#repo}} + <b>{{name}}</b> reviewers:{{#who}} {{reviewer}}{{/who}} committers:{{#who}} {{committer}}{{/who}} +{{/repo}} + +{{#person?}} + Hi {{name}}! +{{/person?}} + +{{=%(% %)%=}} +===================================== +%(%! gros commentaire %)% +%(%#repo%)% + <b>%(%name%)%</b> reviewers:%(%#who%)% %(%reviewer%)%%(%/who%)% committers:%(%#who%)% %(%committer%)%%(%/who%)% +%(%/repo%)% +===================================== +%(%={{ }}=%)% +ggggggggg +{{> special}} +jjjjjjjjj +end + +{{:#sharp}} +{{:!bang}} +{{:~tilde}} +{{:/~0tilde}} +{{:/~1slash}} see json pointers IETF RFC 6901 +{{:^circ}} +{{:\=equal}} +{{::colon}} +{{:>greater}} diff --git a/src/templating/test1/resu.ref b/src/templating/test1/resu.ref new file mode 100644 index 000000000..545e58579 --- /dev/null +++ b/src/templating/test1/resu.ref @@ -0,0 +1,49 @@ +Hello Chris +You have just won 10000 dollars! + +Well, 6000 dollars, after taxes. + +Shown. + + + No person + + + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + + + + Hi Jon! + + + +===================================== + + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + +===================================== + +ggggggggg +----3.14159---- +jjjjjjjjj +end + +# +! +~ +~ +/ see json pointers IETF RFC 6901 +^ += +: +> diff --git a/src/templating/test2/.gitignore b/src/templating/test2/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test2/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test2/json b/src/templating/test2/json new file mode 100644 index 000000000..8c668b3b1 --- /dev/null +++ b/src/templating/test2/json @@ -0,0 +1,9 @@ +{ + "header": "Colors", + "items": [ + {"name": "red", "first": true, "url": "#Red"}, + {"name": "green", "link": true, "url": "#Green"}, + {"name": "blue", "link": true, "url": "#Blue"} + ], + "empty": false +} diff --git a/src/templating/test2/must b/src/templating/test2/must new file mode 100644 index 000000000..aa6da7077 --- /dev/null +++ b/src/templating/test2/must @@ -0,0 +1,17 @@ +<h1>{{header}}</h1> +{{#bug}} +{{/bug}} + +{{#items}} + {{#first}} + <li><strong>{{name}}</strong></li> + {{/first}} + {{#link}} + <li><a href="{{url}}">{{name}}</a></li> + {{/link}} +{{/items}} + +{{#empty}} + <p>The list is empty.</p> +{{/empty}} + diff --git a/src/templating/test2/resu.ref b/src/templating/test2/resu.ref new file mode 100644 index 000000000..67d1f547d --- /dev/null +++ b/src/templating/test2/resu.ref @@ -0,0 +1,22 @@ +<h1>Colors</h1> + + + + + <li><strong>red</strong></li> + + + + + + <li><a href="#Green">green</a></li> + + + + + <li><a href="#Blue">blue</a></li> + + + + + diff --git a/src/templating/test3/.gitignore b/src/templating/test3/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test3/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test3/json b/src/templating/test3/json new file mode 100644 index 000000000..792788171 --- /dev/null +++ b/src/templating/test3/json @@ -0,0 +1,7 @@ +{ + "name": "Chris", + "company": "<b>GitHub & Co</b>", + "names": ["Chris", "Kross"], + "skills": ["JavaScript", "PHP", "Java"], + "age": 18 +} diff --git a/src/templating/test3/must b/src/templating/test3/must new file mode 100644 index 000000000..5c490469b --- /dev/null +++ b/src/templating/test3/must @@ -0,0 +1,15 @@ +* {{name}} +* {{age}} +* {{company}} +* {{&company}} +* {{{company}}} +{{=<% %>=}} +* <%company%> +* <%&company%> +* <%{company}%> + +<%={{ }}=%> +* <ul>{{#names}}<li>{{.}}</li>{{/names}}</ul> +* skills: <ul>{{#skills}}<li>{{.}}</li>{{/skills}}</ul> +{{#age}}* age: {{.}}{{/age}} + diff --git a/src/templating/test3/resu.ref b/src/templating/test3/resu.ref new file mode 100644 index 000000000..e89ce9022 --- /dev/null +++ b/src/templating/test3/resu.ref @@ -0,0 +1,15 @@ +* Chris +* 18 +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> + +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> + + +* <ul><li>Chris</li><li>Kross</li></ul> +* skills: <ul><li>JavaScript</li><li>PHP</li><li>Java</li></ul> +* age: 18 + diff --git a/src/templating/test4/.gitignore b/src/templating/test4/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test4/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test4/json b/src/templating/test4/json new file mode 100644 index 000000000..a10836072 --- /dev/null +++ b/src/templating/test4/json @@ -0,0 +1,13 @@ +{ + "person": { "name": "Jon", "age": 25 }, + "person.name": "Fred", + "person.name=Fred": "The other Fred.", + "persons": [ + { "name": "Jon", "age": 25, "lang": "en" }, + { "name": "Henry", "age": 27, "lang": "en" }, + { "name": "Amed", "age": 24, "lang": "fr" } ], + "fellows": { + "Jon": { "age": 25, "lang": "en" }, + "Henry": { "age": 27, "lang": "en" }, + "Amed": { "age": 24, "lang": "fr" } } +} diff --git a/src/templating/test4/must b/src/templating/test4/must new file mode 100644 index 000000000..003b93666 --- /dev/null +++ b/src/templating/test4/must @@ -0,0 +1,58 @@ +This are extensions!! + +{{person.name}} +{{person.age}} + +{{person\.name}} +{{person\.name\=Fred}} + +{{#person.name=Jon}} +Hello Jon +{{/person.name=Jon}} + +{{^person.name=Jon}} +No Jon? Hey Jon... +{{/person.name=Jon}} + +{{^person.name=Harry}} +No Harry? Hey Calahan... +{{/person.name=Harry}} + +{{#person\.name=Fred}} +Hello Fred +{{/person\.name=Fred}} + +{{^person\.name=Fred}} +No Fred? Hey Fred... +{{/person\.name=Fred}} + +{{#person\.name\=Fred=The other Fred.}} +Hello Fred#2 +{{/person\.name\=Fred=The other Fred.}} + +{{^person\.name\=Fred=The other Fred.}} +No Fred#2? Hey Fred#2... +{{/person\.name\=Fred=The other Fred.}} + +{{#persons}} +{{#lang=!fr}}Hello {{name}}, {{age}} years{{/lang=!fr}} +{{#lang=fr}}Salut {{name}}, {{age}} ans{{/lang=fr}} +{{/persons}} + +{{#persons}} +{{name}}: {{age=24}}/{{age}}/{{age=!27}} +{{/persons}} + +{{#fellows.*}} +{{*}}: {{age=24}}/{{age}}/{{age=!27}} +{{/fellows.*}} + +{{#*}} + (1) {{*}}: {{.}} + {{#*}} + (2) {{*}}: {{.}} + {{#*}} + (3) {{*}}: {{.}} + {{/*}} + {{/*}} +{{/*}} diff --git a/src/templating/test4/resu.ref b/src/templating/test4/resu.ref new file mode 100644 index 000000000..2d48918ac --- /dev/null +++ b/src/templating/test4/resu.ref @@ -0,0 +1,100 @@ +This are extensions!! + +Jon +25 + +Fred +The other Fred. + + +Hello Jon + + + + + +No Harry? Hey Calahan... + + + +Hello Fred + + + + + +Hello Fred#2 + + + + + +Hello Jon, 25 years + + +Hello Henry, 27 years + + + +Salut Amed, 24 ans + + + +Jon: /25/25 + +Henry: /27/ + +Amed: 24/24/24 + + + +Jon: /25/25 + +Henry: /27/ + +Amed: 24/24/24 + + + + (1) person: { "name": "Jon", "age": 25 } + + (2) name: Jon + + + (2) age: 25 + + + + (1) person.name: Fred + + + (1) person.name=Fred: The other Fred. + + + (1) persons: [ { "name": "Jon", "age": 25, "lang": "en" }, { "name": "Henry", "age": 27, "lang": "en" }, { "name": "Amed", "age": 24, "lang": "fr" } ] + + + (1) fellows: { "Jon": { "age": 25, "lang": "en" }, "Henry": { "age": 27, "lang": "en" }, "Amed": { "age": 24, "lang": "fr" } } + + (2) Jon: { "age": 25, "lang": "en" } + + (3) age: 25 + + (3) lang: en + + + (2) Henry: { "age": 27, "lang": "en" } + + (3) age: 27 + + (3) lang: en + + + (2) Amed: { "age": 24, "lang": "fr" } + + (3) age: 24 + + (3) lang: fr + + + diff --git a/src/templating/test5/.gitignore b/src/templating/test5/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test5/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test5/json b/src/templating/test5/json new file mode 100644 index 000000000..5b2e3d83a --- /dev/null +++ b/src/templating/test5/json @@ -0,0 +1,23 @@ +{ + "name": "Chris", + "value": 10000, + "taxed_value": 6000, + "in_ca": true, + "person": false, + "repo": [ + { "name": "resque", "who": [ { "committer": "joe" }, { "reviewer": "avrel" }, { "committer": "william" } ] }, + { "name": "hub", "who": [ { "committer": "jack" }, { "reviewer": "avrel" }, { "committer": "greg" } ] }, + { "name": "rip", "who": [ { "reviewer": "joe" }, { "reviewer": "jack" }, { "committer": "greg" } ] } + ], + "person?": { "name": "Jon" }, + "special": "----{{extra}}----", + "extra": 3.14159, + "#sharp": "#", + "!bang": "!", + "/slash": "/", + "^circ": "^", + "=equal": "=", + ":colon": ":", + ">greater": ">", + "~tilde": "~" +} diff --git a/src/templating/test5/must b/src/templating/test5/must new file mode 100644 index 000000000..44305df24 --- /dev/null +++ b/src/templating/test5/must @@ -0,0 +1,23 @@ +===================================== +from json +{{> special}} +===================================== +not found +{{> notfound}} +===================================== +without extension first +{{> must2 }} +===================================== +last with extension +{{> must3 }} +===================================== +Ensure must3 didn't change specials + +{{#person?}} + Hi {{name}}! +{{/person?}} + +%(%#person?%)% + Hi %(%name%)%! +%(%/person?%)% + diff --git a/src/templating/test5/must2 b/src/templating/test5/must2 new file mode 100644 index 000000000..d4a1d3783 --- /dev/null +++ b/src/templating/test5/must2 @@ -0,0 +1,14 @@ +must2 == BEGIN +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +Shown. +{{#person}} + Never shown! +{{/person}} +{{^person}} + No person +{{/person}} +must2 == END diff --git a/src/templating/test5/must2.mustache b/src/templating/test5/must2.mustache new file mode 100644 index 000000000..33f1ead38 --- /dev/null +++ b/src/templating/test5/must2.mustache @@ -0,0 +1 @@ +must2.mustache ==SHOULD NOT BE SEEN== diff --git a/src/templating/test5/must3.mustache b/src/templating/test5/must3.mustache new file mode 100644 index 000000000..821aaac33 --- /dev/null +++ b/src/templating/test5/must3.mustache @@ -0,0 +1,17 @@ +must3.mustache == BEGIN +{{#repo}} + <b>{{name}}</b> reviewers:{{#who}} {{reviewer}}{{/who}} committers:{{#who}} {{committer}}{{/who}} +{{/repo}} + +{{#person?}} + Hi {{name}}! +{{/person?}} + +{{=%(% %)%=}} +===================================== +%(%! big comment %)% +%(%#repo%)% + <b>%(%name%)%</b> reviewers:%(%#who%)% %(%reviewer%)%%(%/who%)% committers:%(%#who%)% %(%committer%)%%(%/who%)% +%(%/repo%)% +===================================== +must3.mustache == END diff --git a/src/templating/test5/resu.ref b/src/templating/test5/resu.ref new file mode 100644 index 000000000..afc396599 --- /dev/null +++ b/src/templating/test5/resu.ref @@ -0,0 +1,60 @@ +===================================== +from json +----3.14159---- +===================================== +not found + +===================================== +without extension first +must2 == BEGIN +Hello Chris +You have just won 10000 dollars! + +Well, 6000 dollars, after taxes. + +Shown. + + + No person + +must2 == END + +===================================== +last with extension +must3.mustache == BEGIN + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + + + + Hi Jon! + + + +===================================== + + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + +===================================== +must3.mustache == END + +===================================== +Ensure must3 didn't change specials + + + Hi Jon! + + +%(%#person?%)% + Hi %(%name%)%! +%(%/person?%)% + diff --git a/src/templating/test5/special b/src/templating/test5/special new file mode 100644 index 000000000..02d9975c6 --- /dev/null +++ b/src/templating/test5/special @@ -0,0 +1 @@ +special ==SHOULD NOT BE SEEN== diff --git a/src/templating/test5/special.mustache b/src/templating/test5/special.mustache new file mode 100644 index 000000000..70a771fd6 --- /dev/null +++ b/src/templating/test5/special.mustache @@ -0,0 +1 @@ +special.mustache ==SHOULD NOT BE SEEN== diff --git a/src/templating/test6/.gitignore b/src/templating/test6/.gitignore new file mode 100644 index 000000000..62f4d9190 --- /dev/null +++ b/src/templating/test6/.gitignore @@ -0,0 +1,4 @@ +resu.last +vg.last +test-custom-write +!test-custom-write.c diff --git a/src/templating/test6/json b/src/templating/test6/json new file mode 100644 index 000000000..5b2e3d83a --- /dev/null +++ b/src/templating/test6/json @@ -0,0 +1,23 @@ +{ + "name": "Chris", + "value": 10000, + "taxed_value": 6000, + "in_ca": true, + "person": false, + "repo": [ + { "name": "resque", "who": [ { "committer": "joe" }, { "reviewer": "avrel" }, { "committer": "william" } ] }, + { "name": "hub", "who": [ { "committer": "jack" }, { "reviewer": "avrel" }, { "committer": "greg" } ] }, + { "name": "rip", "who": [ { "reviewer": "joe" }, { "reviewer": "jack" }, { "committer": "greg" } ] } + ], + "person?": { "name": "Jon" }, + "special": "----{{extra}}----", + "extra": 3.14159, + "#sharp": "#", + "!bang": "!", + "/slash": "/", + "^circ": "^", + "=equal": "=", + ":colon": ":", + ">greater": ">", + "~tilde": "~" +} diff --git a/src/templating/test6/must b/src/templating/test6/must new file mode 100644 index 000000000..723f966c4 --- /dev/null +++ b/src/templating/test6/must @@ -0,0 +1,43 @@ +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +Shown. +{{#person}} + Never shown! +{{/person}} +{{^person}} + No person +{{/person}} + +{{#repo}} + <b>{{name}}</b> reviewers:{{#who}} {{reviewer}}{{/who}} committers:{{#who}} {{committer}}{{/who}} +{{/repo}} + +{{#person?}} + Hi {{name}}! +{{/person?}} + +{{=%(% %)%=}} +===================================== +%(%! gros commentaire %)% +%(%#repo%)% + <b>%(%name%)%</b> reviewers:%(%#who%)% %(%reviewer%)%%(%/who%)% committers:%(%#who%)% %(%committer%)%%(%/who%)% +%(%/repo%)% +===================================== +%(%={{ }}=%)% +ggggggggg +{{> special}} +jjjjjjjjj +end + +{{:#sharp}} +{{:!bang}} +{{:~tilde}} +{{:/~0tilde}} +{{:/~1slash}} see json pointers IETF RFC 6901 +{{:^circ}} +{{:\=equal}} +{{::colon}} +{{:>greater}} diff --git a/src/templating/test6/resu.ref b/src/templating/test6/resu.ref new file mode 100644 index 000000000..345d3aef6 --- /dev/null +++ b/src/templating/test6/resu.ref @@ -0,0 +1,147 @@ +HELLO CHRIS +YOU HAVE JUST WON 10000 DOLLARS! + +WELL, 6000 DOLLARS, AFTER TAXES. + +SHOWN. + + + NO PERSON + + + + <B>RESQUE</B> REVIEWERS: AVREL COMMITTERS: JOE WILLIAM + + <B>HUB</B> REVIEWERS: AVREL COMMITTERS: JACK GREG + + <B>RIP</B> REVIEWERS: JOE JACK COMMITTERS: GREG + + + + HI JON! + + + +===================================== + + + <B>RESQUE</B> REVIEWERS: AVREL COMMITTERS: JOE WILLIAM + + <B>HUB</B> REVIEWERS: AVREL COMMITTERS: JACK GREG + + <B>RIP</B> REVIEWERS: JOE JACK COMMITTERS: GREG + +===================================== + +GGGGGGGGG +----3.14159---- +JJJJJJJJJ +END + +# +! +~ +~ +/ SEE JSON POINTERS IETF RFC 6901 +^ += +: +> +hello chris +you have just won 10000 dollars! + +well, 6000 dollars, after taxes. + +shown. + + + no person + + + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + + + + hi jon! + + + +===================================== + + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + +===================================== + +ggggggggg +----3.14159---- +jjjjjjjjj +end + +# +! +~ +~ +/ see json pointers ietf rfc 6901 +^ += +: +> +Hello Chris +You have just won 10000 dollars! + +Well, 6000 dollars, after taxes. + +Shown. + + + No person + + + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + + + + Hi Jon! + + + +===================================== + + + <b>resque</b> reviewers: avrel committers: joe william + + <b>hub</b> reviewers: avrel committers: jack greg + + <b>rip</b> reviewers: joe jack committers: greg + +===================================== + +ggggggggg +----3.14159---- +jjjjjjjjj +end + +# +! +~ +~ +/ see json pointers IETF RFC 6901 +^ += +: +> diff --git a/src/templating/test6/test-custom-write.c b/src/templating/test6/test-custom-write.c new file mode 100644 index 000000000..cc50a47cb --- /dev/null +++ b/src/templating/test6/test-custom-write.c @@ -0,0 +1,145 @@ +/* + Author: José Bollo <jobol@nonadev.net> + Author: José Bollo <jose.bollo@iot.bzh> + + https://gitlab.com/jobol/mustach + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#define _GNU_SOURCE + +#include <stdlib.h> +#include <stdio.h> +#include <ctype.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> +#include <libgen.h> + +#include "../mustach-json-c.h" + +static const size_t BLOCKSIZE = 8192; + +static char *readfile(const char *filename) +{ + int f; + struct stat s; + char *result; + size_t size, pos; + ssize_t rc; + + result = NULL; + if (filename[0] == '-' && filename[1] == 0) + f = dup(0); + else + f = open(filename, O_RDONLY); + if (f < 0) { + fprintf(stderr, "Can't open file: %s\n", filename); + exit(1); + } + + fstat(f, &s); + switch (s.st_mode & S_IFMT) { + case S_IFREG: + size = s.st_size; + break; + case S_IFSOCK: + case S_IFIFO: + size = BLOCKSIZE; + break; + default: + fprintf(stderr, "Bad file: %s\n", filename); + exit(1); + } + + pos = 0; + result = malloc(size + 1); + do { + if (result == NULL) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + rc = read(f, &result[pos], (size - pos) + 1); + if (rc < 0) { + fprintf(stderr, "Error while reading %s\n", filename); + exit(1); + } + if (rc > 0) { + pos += (size_t)rc; + if (pos > size) { + size = pos + BLOCKSIZE; + result = realloc(result, size + 1); + } + } + } while(rc > 0); + + close(f); + result[pos] = 0; + return result; +} + +enum { None, Upper, Lower } mode = None; + +int uwrite(void *closure, const char *buffer, size_t size) +{ + switch(mode) { + case None: + fwrite(buffer, size, 1, stdout); + break; + case Upper: + while(size--) + fputc(toupper(*buffer++), stdout); + break; + case Lower: + while(size--) + fputc(tolower(*buffer++), stdout); + break; + } + return 0; +} + +int main(int ac, char **av) +{ + struct json_object *o; + char *t; + char *prog = *av; + int s; + + if (*++av) { + o = json_object_from_file(av[0]); + if (o == NULL) { + fprintf(stderr, "Aborted: null json (file %s)\n", av[0]); + exit(1); + } + while(*++av) { + if (!strcmp(*av, "-U")) + mode = Upper; + else if (!strcmp(*av, "-l")) + mode = Lower; + else if (!strcmp(*av, "-x")) + mode = None; + else { + t = readfile(*av); + s = umustach_json_c(t, o, uwrite, NULL); + if (s != 0) + fprintf(stderr, "Template error %d\n", s); + free(t); + } + } + json_object_put(o); + } + return 0; +} + diff --git a/src/templating/test_mustach_jansson.c b/src/templating/test_mustach_jansson.c new file mode 100644 index 000000000..11af86faa --- /dev/null +++ b/src/templating/test_mustach_jansson.c @@ -0,0 +1,163 @@ +/* + This file is part of TALER + Copyright (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ + +/** + * @file test_mustach_jansson.c + * @brief testcase to test the mustach/jansson integration + * @author Florian Dold + */ +#include "platform.h" +#include "mustach-jansson.h" + + +static void +assert_template (const char *template, + json_t *root, + const char *expected) +{ + char *r; + size_t sz; + + GNUNET_assert (0 == mustach_jansson (template, + root, + &r, + &sz)); + GNUNET_assert (0 == strcmp (r, + expected)); + GNUNET_free (r); +} + + +int +main (int argc, + char *const *argv) +{ + json_t *root = json_object (); + json_t *arr = json_array (); + json_t *obj = json_object (); + json_t *contract; + /* test 1 */ + const char *t1 = "hello world"; + const char *x1 = "hello world"; + /* test 2 */ + const char *t2 = "hello {{ v1 }}"; + const char *x2 = "hello world"; + /* test 3 */ + const char *t3 = "hello {{ v3.x }}"; + const char *x3 = "hello baz"; + /* test 4 */ + const char *t4 = "hello {{# v2 }}{{ . }}{{/ v2 }}"; + const char *x4 = "hello foobar"; + /* test 5 */ + const char *t5 = "hello {{# v3 }}{{ y }}/{{ x }}{{ z }}{{/ v3 }}"; + const char *x5 = "hello quux/baz"; + /* test 6 */ + const char *t6 = "hello {{ v2!stringify }}"; + const char *x6 = "hello [\n \"foo\",\n \"bar\"\n]"; + /* test 7 */ + const char *t7 = "amount: {{ amt!amount_decimal }} {{ amt!amount_currency }}"; + const char *x7 = "amount: 123.00 EUR"; + /* test 8 */ + const char *t8 = "{{^ v4 }}fallback{{/ v4 }}"; + const char *x8 = "fallback"; + + /* contract test 8 (contract) */ + const char *tc = "summary: {{ summary!i18n }}"; + const char *xc_en = "summary: ENGLISH"; + const char *xc_de = "summary: DEUTSCH"; + const char *xc_fr = "summary: FRANCAISE"; + + GNUNET_assert (NULL != root); + GNUNET_assert (NULL != arr); + GNUNET_assert (NULL != obj); + GNUNET_assert (0 == + json_object_set_new (root, + "v1", + json_string ("world"))); + GNUNET_assert (0 == + json_object_set_new (root, + "v4", + json_array ())); + GNUNET_assert (0 == + json_array_append_new (arr, + json_string ("foo"))); + GNUNET_assert (0 == + json_array_append_new (arr, + json_string ("bar"))); + GNUNET_assert (0 == + json_object_set_new (root, + "v2", + arr)); + GNUNET_assert (0 == + json_object_set_new (root, + "v3", + obj)); + GNUNET_assert (0 == + json_object_set_new (root, + "amt", + json_string ("EUR:123.00"))); + GNUNET_assert (0 == + json_object_set_new (obj, + "x", + json_string ("baz"))); + GNUNET_assert (0 == + json_object_set_new (obj, + "y", + json_string ("quux"))); + contract = json_pack ("{ s:s, s:{s:s, s:s}}", + "summary", + "ENGLISH", + "summary_i18n", + "de", + "DEUTSCH", + "fr", + "FRANCAISE"); + GNUNET_assert (NULL != contract); + + assert_template (t1, root, x1); + assert_template (t2, root, x2); + assert_template (t3, root, x3); + assert_template (t4, root, x4); + assert_template (t5, root, x5); + assert_template (t6, root, x6); + assert_template (t7, root, x7); + assert_template (t8, root, x8); + assert_template (tc, contract, xc_en); + + GNUNET_assert (0 == + json_object_set_new (contract, + "$language", + json_string ("de"))); + assert_template (tc, contract, xc_de); + + GNUNET_assert (0 == + json_object_set_new (contract, + "$language", + json_string ("fr"))); + assert_template (tc, contract, xc_fr); + + GNUNET_assert (0 == + json_object_set_new (contract, + "$language", + json_string ("it"))); + assert_template (tc, contract, xc_en); + json_decref (root); + json_decref (contract); + return 0; +} |