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:
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,