exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 813e29a6d31e17ba0b718c5ab12b4c3c14d989a6
parent c40ab311edfc574ed48284dfb82d2904697baf3f
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue,  4 Aug 2026 22:20:13 +0200

add AmountList and spec parser for session IDs

Diffstat:
Mmeson.build | 4++--
Msrc/include/taler/taler_amount_lib.h | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler/taler_json_lib.h | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler/taler_util.h | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/json/json_helper.c | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/json/json_pack.c | 28++++++++++++++++++++++++++++
Msrc/json/test_json.c | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/amount.c | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/config.c | 35+++++++++++++++++++++++++++++++++++
Msrc/util/meson.build | 1+
Msrc/util/test_amount.c | 178++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/util/util.c | 50++++++++++++++++++++++++++++++++++++++++++++++----
12 files changed, 1086 insertions(+), 8 deletions(-)

diff --git a/meson.build b/meson.build @@ -281,8 +281,8 @@ if not get_option('only-doc') libltversions = [ - ['libtalerutil', '16:0:2'], - ['libtalerjson', '8:0:4'], + ['libtalerutil', '17:0:3'], + ['libtalerjson', '9:0:5'], ['libtalercurl', '0:1:0'], ['libtalerpq', '1:0:0'], ['libtalersq', '0:0:0'], diff --git a/src/include/taler/taler_amount_lib.h b/src/include/taler/taler_amount_lib.h @@ -561,6 +561,153 @@ TALER_amount_set_find (const char *currency, const struct TALER_AmountSet *as); +/** + * A list of amounts in distinct currencies, all denoting the + * same thing priced differently --- a price list. + * + * Do not confuse this with a `struct TALER_AmountSet`, which has the + * same memory layout but the opposite invariants: a set is an + * accumulator, a list is a price list. In particular, a currency + * that is absent from a list is *not offered*, which is distinct + * from being offered at zero, and #TALER_amount_list_find() thus + * returns NULL for it instead of synthesizing a zero amount. + * + * Each currency occurs at most once. The order is significant: + * the first entry is the primary currency. + */ +struct TALER_AmountList +{ + /** + * Array of amounts, at most one per currency. + */ + struct TALER_Amount *tal; + + /** + * Length of the @e tal array. + */ + unsigned int tal_len; + +}; + + +/** + * Free memory allocated within @a al, but not @a al itself. + * + * @param[in,out] al list to free (turned into an empty list) + */ +void +TALER_amount_list_free (struct TALER_AmountList *al); + + +/** + * Parse a price list of the form "EUR:1.1;CHF:1;USD:2". + * + * Fails if any component is empty or not a valid amount, or if a + * currency is given more than once. The empty string parses into + * the empty list, which is the canonical way to say "free". + * + * @param str string to parse + * @param[out] al list to initialize, only modified on success; + * the caller must eventually free it using + * #TALER_amount_list_free() + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +enum GNUNET_GenericReturnValue +TALER_string_to_amount_list (const char *str, + struct TALER_AmountList *al); + + +/** + * Convert the price list @a al to a string, the inverse of + * #TALER_string_to_amount_list(). This function is not reentrant, + * repeated calls may overwrite previously returned results. + * + * @param al list to convert + * @return statically allocated buffer with the string representation, + * NULL if any amount in @a al was invalid + */ +const char * +TALER_amount_list2s (const struct TALER_AmountList *al); + + +/** + * Find the price in @a currency in the price list @a al. + * + * Deliberately unlike #TALER_amount_set_find(), a currency that + * @a al does not offer yields NULL and not a zero amount: for a + * price list, "not offered" and "free" are different answers, and + * confusing them would make an unpriced currency look free. + * + * @param al list to search + * @param currency currency to search for + * @return NULL if @a al does not offer @a currency + */ +const struct TALER_Amount * +TALER_amount_list_find (const struct TALER_AmountList *al, + const char *currency); + + +/** + * Check that @a al prices uniformly, that is that it does not + * offer the same thing for free in one currency and for money in + * another. + * + * @param al list to check + * @return #GNUNET_OK if every entry is non-zero (a real price), + * #GNUNET_NO if every entry is zero or @a al is empty (free), + * #GNUNET_SYSERR if some entries are zero and others are not + */ +enum GNUNET_GenericReturnValue +TALER_amount_list_check_uniform (const struct TALER_AmountList *al); + + +/** + * Check that @a al prices in exactly the @a currencies_len + * currencies given in @a currencies, no more and no fewer. + * + * @param al list to check + * @param currencies array of currency names that must be covered + * @param currencies_len length of the @a currencies array + * @return true if @a al holds exactly one entry per currency + * in @a currencies and no others + */ +bool +TALER_amount_list_covers (const struct TALER_AmountList *al, + const char *const *currencies, + unsigned int currencies_len); + + +/** + * Multiply every price in @a al by @a n, for example to turn a + * per-year price into the price of @a n years. + * + * Either all currencies are updated or none is: an overflow in any + * one of them fails the entire call and leaves @a al untouched. + * + * @param[in,out] al list to multiply in place + * @param n factor to multiply by, must not be zero (zero would turn + * a priced list into a free one, which is never intended) + * @return #GNUNET_OK on success, + * #GNUNET_SYSERR on overflow or if @a n is zero + */ +enum GNUNET_GenericReturnValue +TALER_amount_list_multiply (struct TALER_AmountList *al, + uint32_t n); + + +/** + * Make a deep copy of the price list @a src. + * + * @param[out] dst list to initialize, must not hold anything yet; + * the caller must eventually free it using + * #TALER_amount_list_free() + * @param src list to copy + */ +void +TALER_amount_list_copy (struct TALER_AmountList *dst, + const struct TALER_AmountList *src); + + #if 0 /* keep Emacsens' auto-indent happy */ { #endif diff --git a/src/include/taler/taler_json_lib.h b/src/include/taler/taler_json_lib.h @@ -225,6 +225,22 @@ TALER_JSON_pack_amount_array (const char *name, /** + * Generate packer instruction for a JSON field that contains a price + * list (as an array of amount strings). Unlike + * #TALER_JSON_pack_amount_array(), an empty list is emitted as an + * empty array and not as JSON null, because for a price list "empty" + * is the meaningful value "free". + * + * @param name field name + * @param al price list to encode + * @return json pack specification + */ +struct GNUNET_JSON_PackSpec +TALER_JSON_pack_amount_list (const char *name, + const struct TALER_AmountList *al); + + +/** * Generate packer instruction for a JSON field of type * full payto. * @@ -393,6 +409,21 @@ TALER_JSON_spec_amount_any_array (const char *field, /** + * Result specification for a price list. Elements must be strings in + * the usual "CUR:VAL.FRAC" notation, and no currency may appear twice + * --- otherwise a hostile peer could advertise a price twice and have + * #TALER_amount_list_find() and a UI disagree about which one applies. + * Use GNUNET_JSON_parse_free() to release the list. + * + * @param field name of the field to parse + * @param[out] al where to store the price list + */ +struct GNUNET_JSON_Specification +TALER_JSON_spec_amount_list (const char *field, + struct TALER_AmountList *al); + + +/** * Provide specification to parse given JSON object to * a currency specification. * @@ -626,6 +657,37 @@ TALER_JSON_spec_slug_copy (const char *field, /** + * Generate line in parser specification for session IDs + * (see #TALER_is_session_id()). The empty session ID is + * allowed and is the canonical way for a client to say that + * a payment is not bound to any session (wallets do send it + * that way). + * + * @param field name of the field + * @param[out] session_id string to initialize + * @return corresponding field spec + */ +struct GNUNET_JSON_Specification +TALER_JSON_spec_session_id (const char *field, + const char **session_id); + + +/** + * Generate line in parser specification for session IDs that + * must not be empty. Same as #TALER_JSON_spec_session_id(), + * except for requests that only make sense for an actual + * session. + * + * @param field name of the field + * @param[out] session_id string to initialize + * @return corresponding field spec + */ +struct GNUNET_JSON_Specification +TALER_JSON_spec_nonempty_session_id (const char *field, + const char **session_id); + + +/** * Generate line in parser specification for full * "payto://" URIs. * diff --git a/src/include/taler/taler_util.h b/src/include/taler/taler_util.h @@ -188,6 +188,32 @@ TALER_config_get_amount (const struct GNUNET_CONFIGURATION_Handle *cfg, /** + * Obtain a price list of the form "EUR:1.1;CHF:1;USD:2" from the + * configuration file, for options where the same thing may be + * priced in several currencies at once. + * + * A plain "EUR:1.1" is a valid one-element list, so options that + * used to be read with #TALER_config_get_amount() can be moved to + * this function without invalidating existing configurations. + * + * @param cfg configuration to extract data from + * @param section section of the configuration to access + * @param option option of the configuration to access + * @param[out] al set to the price list found in the configuration; + * the caller must eventually free it using + * #TALER_amount_list_free() + * @return #GNUNET_OK on success, + * #GNUNET_NO if not found (@a al is set to the empty list), + * #GNUNET_SYSERR on error + */ +enum GNUNET_GenericReturnValue +TALER_config_get_amount_list (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section, + const char *option, + struct TALER_AmountList *al); + + +/** * Obtain denomination fee structure of a * denomination from configuration file. All * fee options must start with "fee_" and have @@ -485,7 +511,6 @@ TALER_url_is_reserved (char c); * - Underscore: _ * - Period: . * - Colon: : - * - Equals: = (needed for base64-encoded identifiers) * - Tilde: ~ (needed for core banking account usernames) * * Additional restrictions: @@ -502,6 +527,43 @@ TALER_is_slug (const char *slug); /** + * Test if @a session_id is a valid session ID, as used for + * session-based payments at a merchant. + * + * A session ID is deliberately *not* a slug: it may be empty, and it + * may contain '=' as session IDs are frequently base64-encoded + * identifiers (such as the Paivana IDs) that end in padding. + * + * Allowed characters: + * - ASCII letters: a-z A-Z + * - Digits: 0-9 + * - Hyphen: - + * - Underscore: _ + * - Period: . + * - Colon: : + * - Equals: = (needed for base64-encoded identifiers) + * - Tilde: ~ + * + * Additional restrictions: + * - must not be "." or ".." + * - must not contain '/' + * - must not contain percent-encoding '%' + * + * The empty session ID is valid and means that a payment is not bound + * to any session; it is also what the merchant stores in its database + * in that case, and what it puts into the "taler://pay/" URI of an + * order without a session. Requests where only an actual session + * makes sense must thus check for the empty string in addition to + * calling this function. + * + * @param session_id a string to test if it could be a valid session ID + * @return true if @a session_id is well-formed + */ +bool +TALER_is_session_id (const char *session_id); + + +/** * Check if @a lang matches the @a language_pattern, and if so with * which preference. * See also: https://tools.ietf.org/html/rfc7231#section-5.3.1 diff --git a/src/json/json_helper.c b/src/json/json_helper.c @@ -265,6 +265,106 @@ TALER_JSON_spec_amount_any_array (const char *field, /** + * Parse a JSON array of amounts into a price list. + * + * @param cls closure, NULL + * @param root the json array representing the price list + * @param[out] spec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_amount_list (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *spec) +{ + struct TALER_AmountList *al = spec->ptr; + size_t len; + json_t *entry; + size_t idx; + + (void) cls; + al->tal = NULL; + al->tal_len = 0; + if (! json_is_array (root)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + len = json_array_size (root); + if (0 == len) + return GNUNET_OK; /* empty price list: free */ + al->tal = GNUNET_new_array (len, + struct TALER_Amount); + json_array_foreach (root, idx, entry) + { + struct TALER_Amount *a = &al->tal[idx]; + + if (! json_is_string (entry)) + { + GNUNET_break_op (0); + TALER_amount_list_free (al); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_string_to_amount (json_string_value (entry), + a)) + { + GNUNET_break_op (0); + TALER_amount_list_free (al); + return GNUNET_SYSERR; + } + /* only entries below @a idx are initialized so far, and + #TALER_amount_list_find() must not look at the rest */ + al->tal_len = idx; + if (NULL != + TALER_amount_list_find (al, + a->currency)) + { + GNUNET_break_op (0); + TALER_amount_list_free (al); + return GNUNET_SYSERR; + } + al->tal_len = idx + 1; + } + return GNUNET_OK; +} + + +/** + * Cleanup helper for the price list parser. + * + * @param cls closure, NULL + * @param[out] spec where to free the data + */ +static void +clean_amount_list (void *cls, + struct GNUNET_JSON_Specification *spec) +{ + (void) cls; + TALER_amount_list_free (spec->ptr); +} + + +struct GNUNET_JSON_Specification +TALER_JSON_spec_amount_list (const char *field, + struct TALER_AmountList *al) +{ + struct GNUNET_JSON_Specification ret = { + .parser = &parse_amount_list, + .cleaner = &clean_amount_list, + .cls = NULL, + .field = field, + .ptr = al + }; + + GNUNET_assert (NULL != al); + al->tal = NULL; + al->tal_len = 0; + return ret; +} + + +/** * Parse given JSON object to currency spec. * * @param cls closure, NULL @@ -1571,6 +1671,95 @@ TALER_JSON_spec_slug_copy (const char *field, /** + * Parse given JSON object to a session ID. + * + * @param cls closure, NULL + * @param root the json object representing data + * @param[out] spec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_session_id (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *spec) +{ + const char *str; + + (void) cls; + str = json_string_value (root); + if (NULL == str) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (! TALER_is_session_id (str)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + *(const char **) spec->ptr = str; + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_JSON_spec_session_id (const char *field, + const char **session_id) +{ + struct GNUNET_JSON_Specification ret = { + .parser = &parse_session_id, + .field = field, + .ptr = session_id + }; + + *session_id = NULL; + return ret; +} + + +/** + * Parse given JSON object to a session ID that must not be empty. + * + * @param cls closure, NULL + * @param root the json object representing data + * @param[out] spec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_nonempty_session_id (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *spec) +{ + if (GNUNET_OK != + parse_session_id (cls, + root, + spec)) + return GNUNET_SYSERR; + if ('\0' == (*(const char **) spec->ptr)[0]) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_JSON_spec_nonempty_session_id (const char *field, + const char **session_id) +{ + struct GNUNET_JSON_Specification ret = { + .parser = &parse_nonempty_session_id, + .field = field, + .ptr = session_id + }; + + *session_id = NULL; + return ret; +} + + +/** * Parse given JSON object to payto:// URI. * * @param cls closure, NULL diff --git a/src/json/json_pack.c b/src/json/json_pack.c @@ -479,6 +479,34 @@ TALER_JSON_pack_amount_array (const char *name, struct GNUNET_JSON_PackSpec +TALER_JSON_pack_amount_list (const char *name, + const struct TALER_AmountList *al) +{ + struct GNUNET_JSON_PackSpec ps = { + .field_name = name, + }; + json_t *array = json_array (); + + GNUNET_assert (NULL != array); + for (unsigned int i = 0; i<al->tal_len; i++) + { + char *amount_str = TALER_amount_to_string (&al->tal[i]); + json_t *entry; + + GNUNET_assert (NULL != amount_str); + entry = json_string (amount_str); + GNUNET_free (amount_str); + GNUNET_assert (NULL != entry); + GNUNET_assert (0 == + json_array_append_new (array, + entry)); + } + ps.object = array; + return ps; +} + + +struct GNUNET_JSON_PackSpec TALER_JSON_pack_full_payto ( const char *name, const struct TALER_FullPayto payto) diff --git a/src/json/test_json.c b/src/json/test_json.c @@ -106,6 +106,97 @@ test_amount_array (void) } +/** + * Verify JSON packing/parsing for price lists. + * + * @return 0 on success + */ +static int +test_amount_list (void) +{ + struct TALER_AmountList al; + struct TALER_AmountList parsed; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_list ("costs", + &parsed), + GNUNET_JSON_spec_end () + }; + json_t *doc; + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("EUR:1.2;CHF:3.4", + &al)); + doc = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount_list ("costs", + &al)); + GNUNET_assert (NULL != doc); + GNUNET_assert (GNUNET_OK == + GNUNET_JSON_parse (doc, + spec, + NULL, + NULL)); + GNUNET_assert (parsed.tal_len == al.tal_len); + for (unsigned int i = 0; i<al.tal_len; i++) + GNUNET_assert (0 == + TALER_amount_cmp (&al.tal[i], + &parsed.tal[i])); + GNUNET_JSON_parse_free (spec); + json_decref (doc); + TALER_amount_list_free (&al); + + /* an empty list must survive as an empty array, and not + become JSON null: "free" is a meaningful price */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("", + &al)); + doc = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount_list ("costs", + &al)); + GNUNET_assert (NULL != doc); + GNUNET_assert (json_is_array (json_object_get (doc, + "costs"))); + GNUNET_assert (GNUNET_OK == + GNUNET_JSON_parse (doc, + spec, + NULL, + NULL)); + GNUNET_assert (0 == parsed.tal_len); + GNUNET_JSON_parse_free (spec); + json_decref (doc); + TALER_amount_list_free (&al); + + /* a repeated currency on the wire must be rejected, or a peer + could advertise two prices and have us pick a different one + than the user was shown */ + doc = json_pack ("{s:[s,s,s]}", + "costs", + "EUR:1", + "CHF:2", + "EUR:99"); + GNUNET_assert (NULL != doc); + GNUNET_assert (GNUNET_OK != + GNUNET_JSON_parse (doc, + spec, + NULL, + NULL)); + json_decref (doc); + + /* as must a malformed entry */ + doc = json_pack ("{s:[s,s]}", + "costs", + "EUR:1", + "not-an-amount"); + GNUNET_assert (NULL != doc); + GNUNET_assert (GNUNET_OK != + GNUNET_JSON_parse (doc, + spec, + NULL, + NULL)); + json_decref (doc); + return 0; +} + + struct TestPath_Closure { const char **object_ids; @@ -524,6 +615,8 @@ main (int argc, return 1; if (0 != test_amount_array ()) return 1; + if (0 != test_amount_list ()) + return 1; if (0 != test_contract ()) return 2; if (0 != test_json_canon ()) diff --git a/src/util/amount.c b/src/util/amount.c @@ -971,4 +971,247 @@ TALER_amount_set_find (const char *currency, } +/** + * Upper bound on the length of the string representation of a + * single amount: the currency, the ':', the value, the '.', the + * fraction and the '\0'. 24 is sufficient for a uint64_t value + * in decimal. + */ +#define AMOUNT_STR_MAX (TALER_AMOUNT_FRAC_LEN \ + + TALER_CURRENCY_LEN + 3 + 24) + + +void +TALER_amount_list_free (struct TALER_AmountList *al) +{ + GNUNET_array_grow (al->tal, + al->tal_len, + 0); +} + + +enum GNUNET_GenericReturnValue +TALER_string_to_amount_list (const char *str, + struct TALER_AmountList *al) +{ + struct TALER_AmountList tmp = { + .tal = NULL, + .tal_len = 0 + }; + const char *pos = str; + + /* skip leading whitespace, so that an all-whitespace option + value means "free" and not "malformed" */ + while (isspace ( (unsigned char) pos[0])) + pos++; + if ('\0' == pos[0]) + { + al->tal = NULL; + al->tal_len = 0; + return GNUNET_OK; + } + while (1) + { + const char *end = strchr (pos, + (int) ';'); + size_t len = (NULL == end) + ? strlen (pos) + : (size_t) (end - pos); + struct TALER_Amount a; + char *component; + + if (0 == len) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Empty component in amount list `%s'\n", + str); + TALER_amount_list_free (&tmp); + return GNUNET_SYSERR; + } + component = GNUNET_strndup (pos, + len); + if (GNUNET_OK != + TALER_string_to_amount (component, + &a)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid amount `%s' in amount list `%s'\n", + component, + str); + GNUNET_free (component); + TALER_amount_list_free (&tmp); + return GNUNET_SYSERR; + } + GNUNET_free (component); + /* A repeated currency is a typo, not an accumulation: which of + the two prices would apply is anyone's guess. */ + if (NULL != + TALER_amount_list_find (&tmp, + a.currency)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Currency `%s' given more than once in amount list `%s'\n", + a.currency, + str); + TALER_amount_list_free (&tmp); + return GNUNET_SYSERR; + } + GNUNET_array_append (tmp.tal, + tmp.tal_len, + a); + if (NULL == end) + break; + pos = end + 1; + } + *al = tmp; + return GNUNET_OK; +} + + +const char * +TALER_amount_list2s (const struct TALER_AmountList *al) +{ + static TALER_THREAD_LOCAL char *result; + static TALER_THREAD_LOCAL size_t result_size; + size_t need; + size_t off = 0; + + /* one separator per entry is one too many, which covers the '\0' */ + need = (al->tal_len + 1) * (AMOUNT_STR_MAX + 1); + if (need > result_size) + { + GNUNET_free (result); + result = GNUNET_malloc (need); + result_size = need; + } + result[0] = '\0'; + for (unsigned int i = 0; i<al->tal_len; i++) + { + const char *as = TALER_amount2s (&al->tal[i]); + + if (NULL == as) + { + GNUNET_break (0); + return NULL; + } + off += GNUNET_snprintf (&result[off], + result_size - off, + "%s%s", + (0 == i) ? "" : ";", + as); + } + return result; +} + + +const struct TALER_Amount * +TALER_amount_list_find (const struct TALER_AmountList *al, + const char *currency) +{ + for (unsigned int i = 0; i<al->tal_len; i++) + { + const struct TALER_Amount *ali = &al->tal[i]; + + if (0 == strcasecmp (currency, + ali->currency)) + return ali; + } + return NULL; +} + + +enum GNUNET_GenericReturnValue +TALER_amount_list_check_uniform (const struct TALER_AmountList *al) +{ + bool have_zero = false; + bool have_price = false; + + for (unsigned int i = 0; i<al->tal_len; i++) + { + if (TALER_amount_is_zero (&al->tal[i])) + have_zero = true; + else + have_price = true; + } + if (have_zero && have_price) + return GNUNET_SYSERR; + if (have_price) + return GNUNET_OK; + return GNUNET_NO; /* all zero, or empty */ +} + + +bool +TALER_amount_list_covers (const struct TALER_AmountList *al, + const char *const *currencies, + unsigned int currencies_len) +{ + /* @a al has no duplicates, so equal length plus each currency + being present is enough to conclude the two agree exactly */ + if (al->tal_len != currencies_len) + return false; + for (unsigned int i = 0; i<currencies_len; i++) + if (NULL == + TALER_amount_list_find (al, + currencies[i])) + return false; + return true; +} + + +enum GNUNET_GenericReturnValue +TALER_amount_list_multiply (struct TALER_AmountList *al, + uint32_t n) +{ + struct TALER_Amount *tmp; + + if (0 == n) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if ( (1 == n) || + (0 == al->tal_len) ) + return GNUNET_OK; + /* compute into a scratch array first, so that an overflow in a + late currency does not leave the early ones multiplied */ + tmp = GNUNET_new_array (al->tal_len, + struct TALER_Amount); + for (unsigned int i = 0; i<al->tal_len; i++) + { + if (0 > + TALER_amount_multiply (&tmp[i], + &al->tal[i], + n)) + { + GNUNET_free (tmp); + return GNUNET_SYSERR; + } + } + GNUNET_memcpy (al->tal, + tmp, + al->tal_len * sizeof (struct TALER_Amount)); + GNUNET_free (tmp); + return GNUNET_OK; +} + + +void +TALER_amount_list_copy (struct TALER_AmountList *dst, + const struct TALER_AmountList *src) +{ + dst->tal_len = src->tal_len; + if (0 == src->tal_len) + { + dst->tal = NULL; + return; + } + dst->tal = GNUNET_new_array (src->tal_len, + struct TALER_Amount); + GNUNET_memcpy (dst->tal, + src->tal, + src->tal_len * sizeof (struct TALER_Amount)); +} + + /* end of amount.c */ diff --git a/src/util/config.c b/src/util/config.c @@ -58,6 +58,41 @@ TALER_config_get_amount (const struct GNUNET_CONFIGURATION_Handle *cfg, enum GNUNET_GenericReturnValue +TALER_config_get_amount_list (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section, + const char *option, + struct TALER_AmountList *al) +{ + char *str; + + al->tal = NULL; + al->tal_len = 0; + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + option, + &str)) + { + /* may be OK! */ + return GNUNET_NO; + } + if (GNUNET_OK != + TALER_string_to_amount_list (str, + al)) + { + GNUNET_free (str); + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + option, + "invalid list of amounts"); + return GNUNET_SYSERR; + } + GNUNET_free (str); + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue TALER_config_get_denom_fees (const struct GNUNET_CONFIGURATION_Handle *cfg, const char *currency, const char *section, diff --git a/src/util/meson.build b/src/util/meson.build @@ -152,6 +152,7 @@ talerutil_tests = [ 'test_crypto', 'test_crypto_confirmation', 'test_payto', + 'test_slug', 'test_url', ] diff --git a/src/util/test_amount.c b/src/util/test_amount.c @@ -22,6 +22,182 @@ #include "taler/taler_util.h" +/** + * Test the price list (`struct TALER_AmountList`) API. + * + * @return 0 on success + */ +static int +test_amount_list (void) +{ + struct TALER_AmountList al; + struct TALER_AmountList cp; + struct TALER_Amount a; + const struct TALER_Amount *f; + + /* parsing, order preservation, round-trip */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("EUR:1.1;CHF:1;USD:2;JPY:200", + &al)); + GNUNET_assert (4 == al.tal_len); + GNUNET_assert (0 == strcmp ("EUR", + al.tal[0].currency)); + GNUNET_assert (0 == strcmp ("JPY", + al.tal[3].currency)); + GNUNET_assert (0 == strcmp ("EUR:1.1;CHF:1;USD:2;JPY:200", + TALER_amount_list2s (&al))); + + /* lookup is case-insensitive and, crucially, returns NULL for a + currency that is not offered instead of a zero amount */ + f = TALER_amount_list_find (&al, + "chf"); + GNUNET_assert (NULL != f); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("CHF:1", + &a)); + GNUNET_assert (0 == TALER_amount_cmp (&a, + f)); + GNUNET_assert (NULL == + TALER_amount_list_find (&al, + "GBP")); + /* ... unlike the accumulator, which answers "zero" */ + { + struct TALER_AmountSet as = { + .taa = al.tal, + .taa_size = al.tal_len + }; + + f = TALER_amount_set_find ("GBP", + &as); + GNUNET_assert (NULL != f); + GNUNET_assert (TALER_amount_is_zero (f)); + } + + /* coverage */ + { + const char *good[] = { "JPY", "EUR", "USD", "CHF" }; + const char *missing[] = { "EUR", "CHF", "USD" }; + const char *extra[] = { "EUR", "CHF", "USD", "JPY", "GBP" }; + const char *other[] = { "EUR", "CHF", "USD", "GBP" }; + + GNUNET_assert (TALER_amount_list_covers (&al, + good, + 4)); + GNUNET_assert (! TALER_amount_list_covers (&al, + missing, + 3)); + GNUNET_assert (! TALER_amount_list_covers (&al, + extra, + 5)); + GNUNET_assert (! TALER_amount_list_covers (&al, + other, + 4)); + } + + /* copy is deep */ + TALER_amount_list_copy (&cp, + &al); + GNUNET_assert (cp.tal != al.tal); + GNUNET_assert (cp.tal_len == al.tal_len); + GNUNET_assert (0 == strcmp (TALER_amount_list2s (&cp), + "EUR:1.1;CHF:1;USD:2;JPY:200")); + + /* multiplication applies to every currency */ + GNUNET_assert (GNUNET_OK == + TALER_amount_list_multiply (&cp, + 3)); + GNUNET_assert (0 == strcmp ("EUR:3.3;CHF:3;USD:6;JPY:600", + TALER_amount_list2s (&cp))); + /* a factor of one is a no-op, a factor of zero is refused */ + GNUNET_assert (GNUNET_OK == + TALER_amount_list_multiply (&cp, + 1)); + GNUNET_assert (0 == strcmp ("EUR:3.3;CHF:3;USD:6;JPY:600", + TALER_amount_list2s (&cp))); + GNUNET_assert (GNUNET_SYSERR == + TALER_amount_list_multiply (&cp, + 0)); + TALER_amount_list_free (&cp); + + /* overflow in one currency must not multiply the others either */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("EUR:1;CHF:1000000000000", + &cp)); + GNUNET_assert (GNUNET_SYSERR == + TALER_amount_list_multiply (&cp, + 1000000)); + GNUNET_assert (0 == strcmp ("EUR:1;CHF:1000000000000", + TALER_amount_list2s (&cp))); + TALER_amount_list_free (&cp); + + /* uniformity: all priced */ + GNUNET_assert (GNUNET_OK == + TALER_amount_list_check_uniform (&al)); + TALER_amount_list_free (&al); + + /* uniformity: all free */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("EUR:0;CHF:0", + &al)); + GNUNET_assert (GNUNET_NO == + TALER_amount_list_check_uniform (&al)); + TALER_amount_list_free (&al); + + /* uniformity: mixed, the case the callers must refuse */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("EUR:1;CHF:0", + &al)); + GNUNET_assert (GNUNET_SYSERR == + TALER_amount_list_check_uniform (&al)); + TALER_amount_list_free (&al); + + /* the empty list is the canonical spelling of "free" */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("", + &al)); + GNUNET_assert (0 == al.tal_len); + GNUNET_assert (GNUNET_NO == + TALER_amount_list_check_uniform (&al)); + GNUNET_assert (NULL == + TALER_amount_list_find (&al, + "EUR")); + GNUNET_assert (0 == strcmp ("", + TALER_amount_list2s (&al))); + GNUNET_assert (GNUNET_OK == + TALER_amount_list_multiply (&al, + 7)); + TALER_amount_list_free (&al); + + /* a single amount is a valid one-element list, which is what + keeps existing single-currency configurations working */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount_list ("EUR:1.1", + &al)); + GNUNET_assert (1 == al.tal_len); + TALER_amount_list_free (&al); + + /* rejected: a repeated currency ... */ + GNUNET_assert (GNUNET_SYSERR == + TALER_string_to_amount_list ("EUR:1;CHF:1;EUR:2", + &al)); + /* ... an empty component ... */ + GNUNET_assert (GNUNET_SYSERR == + TALER_string_to_amount_list ("EUR:1;;CHF:1", + &al)); + GNUNET_assert (GNUNET_SYSERR == + TALER_string_to_amount_list ("EUR:1;", + &al)); + /* ... and a malformed component */ + GNUNET_assert (GNUNET_SYSERR == + TALER_string_to_amount_list ("EUR:1;CHF", + &al)); + GNUNET_assert (GNUNET_SYSERR == + TALER_string_to_amount_list ("EUR:1;CHF:x", + &al)); + return 0; +} + + int main (int argc, const char *const argv[]) @@ -339,7 +515,7 @@ main (int argc, GNUNET_assert (0 == TALER_amount_divide2 (&a1, &a2)); - return 0; + return test_amount_list (); } diff --git a/src/util/util.c b/src/util/util.c @@ -500,11 +500,8 @@ TALER_is_slug (const char *slug) case '_': case '.': case ':': - case '=': case '~': - /* Note: '=' is needed as base64-encoded identifiers (such as - the Paivana IDs used as merchant session IDs) may end in - padding. '~' is needed as libeufin-bank allows it in account + /* Note: '~' is needed as libeufin-bank allows it in account usernames, which the core banking API exposes as slugs. */ continue; default: @@ -516,6 +513,51 @@ TALER_is_slug (const char *slug) } +bool +TALER_is_session_id (const char *session_id) +{ + const unsigned char *p; + + /* The empty session ID is legal, it means "no session". */ + if ('\0' == session_id[0]) + return true; + + /* Reject special path components, session IDs are used + as path components of "taler://pay/" URIs. */ + if ( (0 == strcmp (session_id, + ".")) || + (0 == strcmp (session_id, + "..")) ) + return false; + + for (p = (const unsigned char *) session_id; '\0' != *p; p++) + { + unsigned char c = *p; + + if (isalnum (c)) + continue; + + switch (c) + { + case '-': + case '_': + case '.': + case ':': + case '=': + case '~': + /* Note: unlike for slugs, '=' is allowed here as + base64-encoded identifiers (such as the Paivana IDs + used as merchant session IDs) may end in padding. */ + continue; + default: + return false; + } + } + + return true; +} + + #ifdef __APPLE__ char * strchrnul (const char *s,