commit 31d407c0843a719bfc25ea6964bab9f466f17808 parent dbd27ecb078bd523de99118ed5a1ea0846de8cbb Author: Christian Grothoff <christian@grothoff.org> Date: Wed, 31 Dec 2025 13:43:31 +0100 properly support price arrays in products Diffstat:
24 files changed, 910 insertions(+), 592 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -27,295 +27,6 @@ #include <taler/taler_dbevents.h> -enum GNUNET_GenericReturnValue -TMH_parse_fractional_string (const char *value, - int64_t *integer_part, - uint32_t *fractional_part) -{ - const char *ptr; - uint64_t integer = 0; - uint32_t frac = 0; - unsigned int digits = 0; - - GNUNET_assert (NULL != integer_part); - GNUNET_assert (NULL != fractional_part); - - if (NULL == value) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - ptr = value; - if ('\0' == *ptr) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if ('-' == *ptr) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (! isdigit ((unsigned char) *ptr)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - while (isdigit ((unsigned char) *ptr)) - { - unsigned int digit = (unsigned int) (*ptr - '0'); - - if (integer > (UINT64_MAX - digit) / 10) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - integer = integer * 10 + digit; - ptr++; - } - if ('.' == *ptr) - { - ptr++; - if ('\0' == *ptr) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - while (isdigit ((unsigned char) *ptr)) - { - unsigned int digit = (unsigned int) (*ptr - '0'); - - if (digits >= MERCHANT_UNIT_FRAC_MAX_DIGITS) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - frac = (uint32_t) (frac * 10 + digit); - digits++; - ptr++; - } - while (digits < MERCHANT_UNIT_FRAC_MAX_DIGITS) - { - frac *= 10; - digits++; - } - } - if ('\0' != *ptr) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (integer > (uint64_t) INT64_MAX) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - *integer_part = integer; - *fractional_part = frac; - return GNUNET_OK; -} - - -enum GNUNET_GenericReturnValue -TMH_process_quantity_inputs (enum TMH_ValueKind kind, - bool allow_fractional, - bool int_missing, - int64_t int_raw, - bool str_missing, - const char *str_raw, - uint64_t *int_out, - uint32_t *frac_out, - const char **error_param) -{ - static char errbuf[128]; - int64_t parsed_int = 0; - uint32_t parsed_frac = 0; - const char *int_field = (TMH_VK_STOCK == kind) - ? "total_stock" - : "quantity"; - const char *str_field = (TMH_VK_STOCK == kind) - ? "unit_total_stock" - : "unit_quantity"; - - *error_param = NULL; - - if (int_missing && str_missing) - { - GNUNET_snprintf (errbuf, - sizeof (errbuf), - "missing %s and %s", - int_field, - str_field); - *error_param = errbuf; - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (! str_missing) - { - if ( (TMH_VK_STOCK == kind) && - (0 == strcmp ("-1", - str_raw)) ) - { - parsed_int = -1; - parsed_frac = 0; - } - else - { - if (GNUNET_OK != - TMH_parse_fractional_string (str_raw, - &parsed_int, - &parsed_frac)) - { - GNUNET_snprintf (errbuf, - sizeof (errbuf), - "malformed %s", - str_field); - *error_param = errbuf; - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - } - - if ( (! int_missing) && (! str_missing) ) - { - if ( (parsed_int != int_raw) || (0 != parsed_frac) ) - { - GNUNET_snprintf (errbuf, - sizeof (errbuf), - "%s/%s mismatch", - int_field, - str_field); - *error_param = errbuf; - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - else if (int_missing) - { - int_raw = parsed_int; - } - - if ( (TMH_VK_STOCK == kind) && (-1 == int_raw) ) - { - if ( (! str_missing) && (0 != parsed_frac) ) - { - GNUNET_snprintf (errbuf, - sizeof (errbuf), - "fractional part forbidden with %s='-1'", - str_field); - *error_param = errbuf; - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - *int_out = INT64_MAX; - *frac_out = INT32_MAX; - return GNUNET_OK; - } - - if (int_raw < 0) - { - GNUNET_snprintf (errbuf, - sizeof (errbuf), - "%s must be non-negative", - int_field); - *error_param = errbuf; - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (! allow_fractional) - { - if ( (! str_missing) && (0 != parsed_frac) ) - { - GNUNET_snprintf (errbuf, - sizeof (errbuf), - "fractional part not allowed for %s", - str_field); - *error_param = errbuf; - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - parsed_frac = 0; - } - else if (! str_missing) - { - if (parsed_frac >= MERCHANT_UNIT_FRAC_BASE) - { - GNUNET_snprintf (errbuf, - sizeof (errbuf), - "%s fractional part exceeds base %u", - str_field, - MERCHANT_UNIT_FRAC_BASE); - *error_param = errbuf; - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - - *int_out = (uint64_t) int_raw; - *frac_out = parsed_frac; - return GNUNET_OK; -} - - -void -TMH_format_fractional_string (enum TMH_ValueKind kind, - uint64_t integer, - uint32_t fractional, - size_t buffer_length, - char buffer[static buffer_length]) -{ - GNUNET_assert (0 < buffer_length); - - if ( (TMH_VK_STOCK == kind) && - (INT64_MAX == (int64_t) integer) && - (INT32_MAX == (int32_t) fractional) ) - { - GNUNET_snprintf (buffer, - buffer_length, - "-1"); - return; - } - - GNUNET_assert ( (TMH_VK_QUANTITY != kind) || - ((INT64_MAX != (int64_t) integer) && - (INT32_MAX != (int32_t) fractional)) ); - GNUNET_assert (fractional < MERCHANT_UNIT_FRAC_BASE); - - if (0 == fractional) - { - GNUNET_snprintf (buffer, - buffer_length, - "%lu", - integer); - return; - } - { - char frac_buf[MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; - size_t idx; - - GNUNET_snprintf (frac_buf, - sizeof (frac_buf), - "%0*u", - MERCHANT_UNIT_FRAC_MAX_DIGITS, - (unsigned int) fractional); - for (idx = strlen (frac_buf); idx > 0; idx--) - { - if ('0' != frac_buf[idx - 1]) - break; - frac_buf[idx - 1] = '\0'; - } - GNUNET_snprintf (buffer, - buffer_length, - "%lu.%s", - integer, - frac_buf); - } -} - - void TMH_quantity_defaults_from_unit (const struct TMH_MerchantInstance *mi, const char *unit, @@ -735,7 +446,7 @@ TMH_products_array_valid (const json_t *products) } } if ( (NULL != image_data_url) && - (! TMH_image_data_url_valid (image_data_url)) ) + (! TALER_MERCHANT_image_data_url_valid (image_data_url)) ) { GNUNET_break_op (0); valid = false; @@ -756,34 +467,6 @@ TMH_products_array_valid (const json_t *products) bool -TMH_image_data_url_valid (const char *image_data_url) -{ - if (0 == strcmp (image_data_url, - "")) - return true; - if (0 != strncasecmp ("data:image/", - image_data_url, - strlen ("data:image/"))) - { - GNUNET_break_op (0); - return false; - } - if (NULL == strstr (image_data_url, - ";base64,")) - { - GNUNET_break_op (0); - return false; - } - if (! TALER_url_valid_charset (image_data_url)) - { - GNUNET_break_op (0); - return false; - } - return true; -} - - -bool TMH_template_contract_valid (const json_t *template_contract) { const char *summary; diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h @@ -77,64 +77,6 @@ TMH_products_array_valid (const json_t *products); /** - * Parse decimal quantity expressed as string for request handling. - * - * @param value string to parse - * @param[out] integer_part result integer component - * @param[out] fractional_part result fractional component (0..MERCHANT_UNIT_FRAC_BASE-1) - * @return #GNUNET_OK on success, #GNUNET_SYSERR on validation failure - */ -enum GNUNET_GenericReturnValue -TMH_parse_fractional_string (const char *value, - int64_t *integer_part, - uint32_t *fractional_part); - -/** - * Kind of fixed-decimal value the helpers operate on. - * Primarily distinguishes how special sentinel values (such as "-1" - * meaning infinity for stock) must be encoded. - */ -enum TMH_ValueKind -{ - TMH_VK_QUANTITY, /* -1 is illegal */ - TMH_VK_STOCK /* -1 means "infinity" */ -}; - -/** - * Extract a fixed-decimal number that may be supplied either - * - as pure integer (e.g. "total_stock"), or - * - as decimal text (e.g. "unit_total_stock"). - * - * Rules: - * - If both forms are missing -> error. - * - If both are present -> they must match and the decimal must have no fraction. - * - For kind == TMH_VK_STOCK the integer value -1 represents infinity. - * - * @param kind See #TMH_ValueKind - * @param allow_fractional False: any fractional part is rejected - * @param int_missing True if client omitted the integer field - * @param int_raw Raw integer (undefined if @a int_missing is true) - * @param str_missing True if client omitted the string field - * @param str_raw Raw UTF-8 string (undefined if @a str_missing is true) - * @param[out] int_out Canonicalised integer part - * @param[out] frac_out Canonicalised fractional part - * @param[out] error_param Set to offending field name on failure - * @param int_field Integer field name (for error reporting) - * @param str_field String field name (for error reporting) - * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise - */ -enum GNUNET_GenericReturnValue -TMH_process_quantity_inputs (enum TMH_ValueKind kind, - bool allow_fractional, - bool int_missing, - int64_t int_raw, - bool str_missing, - const char *str_raw, - uint64_t *int_out, - uint32_t *frac_out, - const char **error_param); - -/** * Lookup the defaults for @a unit within @a mi and fall back to sane * values (disallow fractional quantities, zero precision) if no data * is available. @@ -167,39 +109,6 @@ TMH_unit_defaults_for_instance (const struct TMH_MerchantInstance *mi, bool *allow_fractional, uint32_t *precision_level); -/** - * Format a fixed-decimal pair into canonical string representation. - * Recognises INT64_MAX / INT32_MAX as the "-1" sentinel used for - * infinite stock if @a kind equals #TMH_VK_STOCK. - * - * @param kind specifies whether sentinel values are permitted - * @param integer integer portion - * @param fractional fractional portion (0..MERCHANT_UNIT_FRAC_BASE-1 or sentinel) - * @param buffer output buffer - * @param buffer_length length of @a buffer - */ -void -TMH_format_fractional_string (enum TMH_ValueKind kind, - uint64_t integer, - uint32_t fractional, - size_t buffer_length, - char buffer[static buffer_length]); - -/** - * Check if @a image_data_url is a valid image - * data URL. Does not validate the actual payload, - * only the syntax and that it properly claims to - * be an image. - * - * FIXME: move to libtalermerchantutil and use in TALER_MERCHANT_parse_product? - * - * @param image_data_url string to check - * @return true if @a image_data_url is a data - * URL with an "image/" mime-type - */ -bool -TMH_image_data_url_valid (const char *image_data_url); - /** * Check if @a template_contract is a valid template_contract object in the sense of Taler's API diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -2282,7 +2282,7 @@ phase_compute_money_pots (struct PayContext *pc) { const struct TALER_MERCHANT_Contract *contract = pc->check_contract.contract_terms; - struct TALER_Amount unassigned; + struct TALER_Amount assigned; if (0 == pc->parse_pay.coins_cnt) { @@ -2293,41 +2293,70 @@ phase_compute_money_pots (struct PayContext *pc) } TALER_amount_set_zero (pc->parse_pay.dc[0].cdd.amount.currency, - &unassigned); + &assigned); GNUNET_assert (NULL != contract); for (size_t i = 0; i<contract->products_len; i++) { - const struct TALER_MERCHANT_Product *product + const struct TALER_MERCHANT_ProductSold *product = &contract->products[i]; - // FIXME: handle products with multiple prices! - const struct TALER_Amount *price = &product->price; + const struct TALER_Amount *price = NULL; - if (GNUNET_OK != - TALER_amount_cmp_currency (&unassigned, - price)) + /* find price in the right currency */ + for (unsigned int j = 0; j<product->prices_length; j++) { - GNUNET_break (0); + if (GNUNET_OK == + TALER_amount_cmp_currency (&assigned, + &product->prices[j])) + { + price = &product->prices[j]; + break; + } + } + if (NULL == price) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Product `%s' has no price given in `%s'.\n", + product->product_id, + assigned.currency); continue; } - if (0 == product->product_money_pot) + if (0 != product->product_money_pot) { GNUNET_assert (0 <= - TALER_amount_add (&unassigned, - &unassigned, + TALER_amount_add (&assigned, + &assigned, price)); - } - else - { increment_pot (pc, product->product_money_pot, price); } } - if ( (! TALER_amount_is_zero (&unassigned)) && - (0 != contract->default_money_pot) ) - increment_pot (pc, - contract->default_money_pot, - &unassigned); + + { + /* Compute what is left from the order total and account for that. + Also sanity-check and handle the case where the overall order + is below that of the sum of the products. */ + struct TALER_Amount left; + + if (0 > + TALER_amount_subtract (&left, + &pc->validate_tokens.brutto, + &assigned)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Total order brutto amount below sum from products, skipping per-product money pots\n"); + GNUNET_free (pc->compute_money_pots.pots); + GNUNET_free (pc->compute_money_pots.increments); + pc->compute_money_pots.num_pots = 0; + left = pc->validate_tokens.brutto; + } + + if ( (! TALER_amount_is_zero (&left)) && + (0 != contract->default_money_pot) ) + increment_pot (pc, + contract->default_money_pot, + &left); + } pc->phase++; } diff --git a/src/backend/taler-merchant-httpd_private-get-pos.c b/src/backend/taler-merchant-httpd_private-get-pos.c @@ -119,11 +119,12 @@ add_product (void *cls, total_stock_api = -1; else total_stock_api = (int64_t) pd->total_stock; - TMH_format_fractional_string (TMH_VK_STOCK, - pd->total_stock, - pd->total_stock_frac, - sizeof (unit_total_stock_buf), - unit_total_stock_buf); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_STOCK, + pd->total_stock, + pd->total_stock_frac, + sizeof (unit_total_stock_buf), + unit_total_stock_buf); GNUNET_assert ( 0 == diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c @@ -87,11 +87,12 @@ TMH_private_get_products_ID ( else total_stock_api = (int64_t) pd.total_stock; - TMH_format_fractional_string (TMH_VK_STOCK, - pd.total_stock, - pd.total_stock_frac, - sizeof (unit_total_stock_buf), - unit_total_stock_buf); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_STOCK, + pd.total_stock, + pd.total_stock_frac, + sizeof (unit_total_stock_buf), + unit_total_stock_buf); ret = TALER_MHD_REPLY_JSON_PACK ( connection, diff --git a/src/backend/taler-merchant-httpd_private-patch-instances-ID.c b/src/backend/taler-merchant-httpd_private-patch-instances-ID.c @@ -143,7 +143,7 @@ patch_instances_ID (struct TMH_MerchantInstance *mi, "address"); } if ( (NULL != is.logo) && - (! TMH_image_data_url_valid (is.logo)) ) + (! TALER_MERCHANT_image_data_url_valid (is.logo)) ) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c @@ -231,15 +231,16 @@ TMH_private_patch_products_ID ( { const char *eparam; if (GNUNET_OK != - TMH_process_quantity_inputs (TMH_VK_STOCK, - unit_allow_fraction, - total_stock_missing, - total_stock, - unit_total_stock_missing, - unit_total_stock, - &pd.total_stock, - &pd.total_stock_frac, - &eparam)) + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_STOCK, + unit_allow_fraction, + total_stock_missing, + total_stock, + unit_total_stock_missing, + unit_total_stock, + &pd.total_stock, + &pd.total_stock_frac, + &eparam)) { ret = TALER_MHD_reply_with_error ( connection, @@ -312,7 +313,7 @@ TMH_private_patch_products_ID ( if (NULL == pd.image) pd.image = (char *) ""; - if (! TMH_image_data_url_valid (pd.image)) + if (! TALER_MERCHANT_image_data_url_valid (pd.image)) { GNUNET_break_op (0); ret = TALER_MHD_reply_with_error (connection, diff --git a/src/backend/taler-merchant-httpd_private-post-instances.c b/src/backend/taler-merchant-httpd_private-post-instances.c @@ -229,7 +229,7 @@ post_instances (const struct TMH_RequestHandler *rh, } if ( (NULL != is.logo) && - (! TMH_image_data_url_valid (is.logo)) ) + (! TALER_MERCHANT_image_data_url_valid (is.logo)) ) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -343,9 +343,9 @@ struct OrderContext size_t products_len; /** - * Array of products that are part of the purchase. + * Array of products that are being sold. */ - struct TALER_MERCHANT_Product *products; + struct TALER_MERCHANT_ProductSold *products; /** * URL where the same contract could be ordered again (if available). @@ -1008,7 +1008,7 @@ clean_order (void *cls) 0); for (size_t i = 0; i<oc->parse_order.products_len; i++) { - TALER_MERCHANT_product_free (&oc->parse_order.products[i]); + TALER_MERCHANT_product_sold_free (&oc->parse_order.products[i]); } GNUNET_free (oc->parse_order.products); oc->parse_order.products_len = 0; @@ -1070,7 +1070,7 @@ clean_order (void *cls) * * @param pd product details with current totals/sold/lost * @param[out] available_value remaining whole units (normalized, non-negative) - * @param[out] available_frac remaining fractional units (0..MERCHANT_UNIT_FRAC_BASE-1) + * @param[out] available_frac remaining fractional units (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) */ static void compute_available_quantity (const struct TALER_MERCHANTDB_ProductDetails *pd, @@ -1100,16 +1100,16 @@ compute_available_quantity (const struct TALER_MERCHANTDB_ProductDetails *pd, if (frac < 0) { - int64_t borrow = ((-frac) + MERCHANT_UNIT_FRAC_BASE - 1) - / MERCHANT_UNIT_FRAC_BASE; + int64_t borrow = ((-frac) + TALER_MERCHANT_UNIT_FRAC_BASE - 1) + / TALER_MERCHANT_UNIT_FRAC_BASE; value -= borrow; - frac += borrow * (int64_t) MERCHANT_UNIT_FRAC_BASE; + frac += borrow * (int64_t) TALER_MERCHANT_UNIT_FRAC_BASE; } - else if (frac >= MERCHANT_UNIT_FRAC_BASE) + else if (frac >= TALER_MERCHANT_UNIT_FRAC_BASE) { - int64_t carry = frac / MERCHANT_UNIT_FRAC_BASE; + int64_t carry = frac / TALER_MERCHANT_UNIT_FRAC_BASE; value += carry; - frac -= carry * (int64_t) MERCHANT_UNIT_FRAC_BASE; + frac -= carry * (int64_t) TALER_MERCHANT_UNIT_FRAC_BASE; } if (value < 0) @@ -1424,16 +1424,18 @@ phase_execute_order (struct OrderContext *oc) compute_available_quantity (&pd, &available_quantity, &available_quantity_frac); - TMH_format_fractional_string (TMH_VK_QUANTITY, - ip->quantity, - ip->quantity_frac, - sizeof (requested_quantity_buf), - requested_quantity_buf); - TMH_format_fractional_string (TMH_VK_QUANTITY, - available_quantity, - available_quantity_frac, - sizeof (available_quantity_buf), - available_quantity_buf); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_QUANTITY, + ip->quantity, + ip->quantity_frac, + sizeof (requested_quantity_buf), + requested_quantity_buf); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_QUANTITY, + available_quantity, + available_quantity_frac, + sizeof (available_quantity_buf), + available_quantity_buf); ret = TALER_MHD_REPLY_JSON_PACK ( oc->connection, MHD_HTTP_GONE, @@ -3338,7 +3340,7 @@ phase_merge_inventory (struct OrderContext *oc) 0 == json_array_append_new ( oc->merge_inventory.products, - TALER_MERCHANT_product_serialize (&oc->parse_order.products[i]))); + TALER_MERCHANT_product_sold_serialize (&oc->parse_order.products[i]))); if (0 != oc->parse_order.products[i].product_money_pot) pots[pots_off++] = oc->parse_order.products[i].product_money_pot; } @@ -3477,15 +3479,16 @@ phase_merge_inventory (struct OrderContext *oc) return; } if (GNUNET_OK != - TMH_process_quantity_inputs (TMH_VK_QUANTITY, - pd.allow_fractional_quantity, - ip->quantity_missing, - (int64_t) ip->quantity, - ip->unit_quantity_missing, - ip->unit_quantity, - &ip->quantity, - &ip->quantity_frac, - &eparam)) + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_QUANTITY, + pd.allow_fractional_quantity, + ip->quantity_missing, + (int64_t) ip->quantity, + ip->unit_quantity_missing, + ip->unit_quantity, + &ip->quantity, + &ip->quantity_frac, + &eparam)) { GNUNET_break_op (0); reply_with_error (oc, @@ -3500,14 +3503,15 @@ phase_merge_inventory (struct OrderContext *oc) json_t *p; char unit_quantity_buf[64]; - TMH_format_fractional_string (TMH_VK_QUANTITY, - ip->quantity, - ip->quantity_frac, - sizeof (unit_quantity_buf), - unit_quantity_buf); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_QUANTITY, + ip->quantity, + ip->quantity_frac, + sizeof (unit_quantity_buf), + unit_quantity_buf); if (0 != pd.money_pot_id) pots[pots_off++] = pd.money_pot_id; - // FIXME: reuse TALER_MERCHANT_product_serialize() here!? + // FIXME: reuse TALER_MERCHANT_product_sold_serialize() here!? p = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("product_name", pd.product_name), @@ -3994,12 +3998,12 @@ phase_parse_order (struct OrderContext *oc) oc->parse_order.products = GNUNET_new_array (oc->parse_order.products_len, - struct TALER_MERCHANT_Product); + struct TALER_MERCHANT_ProductSold); json_array_foreach (products, i, p) { if (GNUNET_OK != - TALER_MERCHANT_parse_product (p, - &oc->parse_order.products[i])) + TALER_MERCHANT_parse_product_sold (p, + &oc->parse_order.products[i])) { GNUNET_break_op (0); reply_with_error (oc, diff --git a/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c @@ -140,15 +140,16 @@ TMH_private_post_products_ID_lock ( { const char *eparam; if (GNUNET_OK != - TMH_process_quantity_inputs (TMH_VK_QUANTITY, - pd.allow_fractional_quantity, - quantity_missing, - (int64_t) quantity, - unit_quantity_missing, - unit_quantity, - &normalized_quantity, - &normalized_quantity_frac, - &eparam)) + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_QUANTITY, + pd.allow_fractional_quantity, + quantity_missing, + (int64_t) quantity, + unit_quantity_missing, + unit_quantity, + &normalized_quantity, + &normalized_quantity_frac, + &eparam)) { TALER_MERCHANTDB_product_details_free (&pd); GNUNET_break_op (0); diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c @@ -229,15 +229,16 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, { const char *eparam; if (GNUNET_OK != - TMH_process_quantity_inputs (TMH_VK_STOCK, - unit_allow_fraction, - total_stock_missing, - total_stock, - unit_total_stock_missing, - unit_total_stock, - &pd.total_stock, - &pd.total_stock_frac, - &eparam)) + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_STOCK, + unit_allow_fraction, + total_stock_missing, + total_stock, + unit_total_stock_missing, + unit_total_stock, + &pd.total_stock, + &pd.total_stock_frac, + &eparam)) { ret = TALER_MHD_reply_with_error ( connection, @@ -310,7 +311,7 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, if (NULL == pd.image) pd.image = (char *) ""; - if (! TMH_image_data_url_valid (pd.image)) + if (! TALER_MERCHANT_image_data_url_valid (pd.image)) { GNUNET_break_op (0); ret = TALER_MHD_reply_with_error (connection, diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -1982,7 +1982,7 @@ TALER_MERCHANT_products_post3 ( * @param taxes list of taxes paid by the merchant * @param total_stock integer quantity to use when @a unit_allow_fraction is false; * set to -1 for unlimited stock - * @param total_stock_frac fractional component (0-#MERCHANT_UNIT_FRAC_BASE) to use when + * @param total_stock_frac fractional component (0-#TALER_MERCHANT_UNIT_FRAC_BASE) to use when * @a unit_allow_fraction is true; ignored otherwise. Clients should use 0 * when there is no fractional part or when @a total_stock is -1 (unlimited). * @param unit_allow_fraction whether fractional quantity purchases are allowed @@ -2112,7 +2112,7 @@ TALER_MERCHANT_product_patch ( * @param image base64-encoded image * @param taxes list of taxes * @param total_stock integer quantity when @a unit_allow_fraction is false - * @param total_stock_frac fractional quantity component (0-#MERCHANT_UNIT_FRAC_BASE) when + * @param total_stock_frac fractional quantity component (0-#TALER_MERCHANT_UNIT_FRAC_BASE) when * @a unit_allow_fraction is true; ignored otherwise * @param unit_allow_fraction whether fractional quantity purchases are allowed * @param unit_precision_level optional override for the fractional precision; pass NULL to use the default derived from @a unit diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -32,8 +32,8 @@ * Fixed-point base for inventory quantities (powers of 10). * Six decimal digits are supported to match the maximum unit precision. */ -#define MERCHANT_UNIT_FRAC_BASE 1000000U -#define MERCHANT_UNIT_FRAC_MAX_DIGITS 6U +#define TALER_MERCHANT_UNIT_FRAC_BASE 1000000U +#define TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS 6U /** * Return default project data used by Taler merchant. @@ -43,6 +43,104 @@ TALER_MERCHANT_project_data (void); /** + * Kind of fixed-decimal value the helpers operate on. + * Primarily distinguishes how special sentinel values (such as "-1" + * meaning infinity for stock) must be encoded. + */ +enum TALER_MERCHANT_ValueKind +{ + TALER_MERCHANT_VK_QUANTITY, /* -1 is illegal */ + TALER_MERCHANT_VK_STOCK /* -1 means "infinity" */ +}; + + +/** + * Parse decimal quantity expressed as string for request handling. + * + * @param value string to parse + * @param[out] integer_part result integer component + * @param[out] fractional_part result fractional component (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) + * @return #GNUNET_OK on success, #GNUNET_SYSERR on validation failure + */ +enum GNUNET_GenericReturnValue +TALER_MERCHANT_vk_parse_fractional_string ( + const char *value, + int64_t *integer_part, + uint32_t *fractional_part); + + +/** + * Extract a fixed-decimal number that may be supplied either + * - as pure integer (e.g. "total_stock"), or + * - as decimal text (e.g. "unit_total_stock"). + * + * Rules: + * - If both forms are missing -> error. + * - If both are present -> they must match and the decimal must have no fraction. + * - For kind == TALER_MERCHANT_VK_STOCK the integer value -1 represents infinity. + * + * @param kind See #TALER_MERCHANT_ValueKind + * @param allow_fractional False: any fractional part is rejected + * @param int_missing True if client omitted the integer field + * @param int_raw Raw integer (undefined if @a int_missing is true) + * @param str_missing True if client omitted the string field + * @param str_raw Raw UTF-8 string (undefined if @a str_missing is true) + * @param[out] int_out Canonicalised integer part + * @param[out] frac_out Canonicalised fractional part + * @param[out] error_param Set to offending field name on failure + * @param int_field Integer field name (for error reporting) + * @param str_field String field name (for error reporting) + * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise + */ +enum GNUNET_GenericReturnValue +TALER_MERCHANT_vk_process_quantity_inputs ( + enum TALER_MERCHANT_ValueKind kind, + bool allow_fractional, + bool int_missing, + int64_t int_raw, + bool str_missing, + const char *str_raw, + uint64_t *int_out, + uint32_t *frac_out, + const char **error_param); + +/** + * Format a fixed-decimal pair into canonical string representation. + * Recognises INT64_MAX / INT32_MAX as the "-1" sentinel used for + * infinite stock if @a kind equals #TALER_MERCHANT_VK_STOCK. + * + * @param kind specifies whether sentinel values are permitted + * @param integer integer portion + * @param fractional fractional portion (0..TALER_MERCHANT_UNIT_FRAC_BASE-1 or sentinel) + * @param buffer output buffer + * @param buffer_length length of @a buffer + */ +void +TALER_MERCHANT_vk_format_fractional_string ( + enum TALER_MERCHANT_ValueKind kind, + uint64_t integer, + uint32_t fractional, + size_t buffer_length, + char buffer[static buffer_length]); + + +/** + * Check if @a image_data_url is a valid image + * data URL. Does not validate the actual payload, + * only the syntax and that it properly claims to + * be an image. + * + * FIXME: use in TALER_MERCHANT_parse_product! + * + * @param image_data_url string to check + * @return true if @a image_data_url is a data + * URL with an "image/" mime-type + */ +bool +TALER_MERCHANT_image_data_url_valid (const char *image_data_url); + + +/** * Channel used to transmit MFA authorization request. */ enum TALER_MERCHANT_MFA_Channel @@ -614,11 +712,29 @@ struct TALER_MERCHANT_ContractTokenFamily } details; }; +/** + * Specifies the quantity of a product (to be) sold. + */ +struct TALER_MERCHANT_ProductQuantity +{ + /** + * Integer component of the quantity. + */ + uint64_t integer; + + /** + * Fractional component of the quantity, in the + * range of 0..TALER_MERCHANT_UNIT_FRAC_BASE-1. + */ + uint32_t fractional; + +}; + /** - * Details about a product to be sold. + * Details about a product (to be) sold. */ -struct TALER_MERCHANT_Product +struct TALER_MERCHANT_ProductSold { /** @@ -643,16 +759,9 @@ struct TALER_MERCHANT_Product json_t *description_i18n; /** - * Legacy integer portion of the quantity to deliver; - * 0 if @e unit_quantity is be used. - * FIXME: deprecated? If so, when? - */ - uint64_t quantity; - - /** - * Preferred quantity string using "<integer>[.<fraction>]" syntax with up to six fractional digits. NULL if @e quantity is used. + * Quantity (in multiples of @e unit) to be sold. */ - char *unit_quantity; + struct TALER_MERCHANT_ProductQuantity unit_quantity; /** * Unit in which the product is measured (liters, kilograms, packages, @@ -661,13 +770,27 @@ struct TALER_MERCHANT_Product char *unit; /** - * The price of the product; this is the total price for quantity times unit - * of this product. - * TODO: possible interpretations will likely change in the future - * once we add taxes, need is_net_price indicator! + * Length of the @e prices array. + */ + unsigned int prices_length; + + /** + * Array of prices (in different currencies) for @e unit_quantity + * units of this product (these are longer the per-unit prices!). + */ + struct TALER_Amount *prices; + + /** + * True if the @e prices given are the net price, + * false if they are the gross price. Note that even @e prices are the + * gross price, @e taxes may be missing if the merchant configured + * gross @e prices but did not configure any @e taxes. + * Similarly, the merchant may have configured net @e prices + * for products but deals with taxes on a per-order basis. Thus, it + * may not always be possible to compute the gross price from the net + * price for an individual product, necessitating this flag. */ - struct TALER_Amount price; - // FIXME: modern product has a price *array*! + bool prices_are_net; /** * An optional base64-encoded image of the product. @@ -788,7 +911,7 @@ struct TALER_MERCHANT_Contract /** * Array of products that are part of the purchase. */ - struct TALER_MERCHANT_Product *products; + struct TALER_MERCHANT_ProductSold *products; /** * Timestamp of the contract. @@ -986,13 +1109,13 @@ TALER_MERCHANT_parse_choice_input ( * Parse JSON product given in @a p, returning the result in * @a r. * - * @param p JSON specifying a ``Product`` to parse + * @param p JSON specifying a ``ProductSold`` to parse * @param[out] r where to write the result * @return #GNUNET_OK on success */ enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_product (const json_t *p, - struct TALER_MERCHANT_Product *r); +TALER_MERCHANT_parse_product_sold (const json_t *p, + struct TALER_MERCHANT_ProductSold *r); /** @@ -1032,8 +1155,8 @@ TALER_MERCHANT_parse_choice_output ( * @return JSON object representing the product @a p */ json_t * -TALER_MERCHANT_product_serialize ( - const struct TALER_MERCHANT_Product *p); +TALER_MERCHANT_product_sold_serialize ( + const struct TALER_MERCHANT_ProductSold *p); /** @@ -1111,7 +1234,7 @@ TALER_MERCHANT_contract_choice_free ( * @param[in] product data structure to clean up */ void -TALER_MERCHANT_product_free (struct TALER_MERCHANT_Product *product); +TALER_MERCHANT_product_sold_free (struct TALER_MERCHANT_ProductSold *product); /** @@ -1122,4 +1245,5 @@ TALER_MERCHANT_product_free (struct TALER_MERCHANT_Product *product); void TALER_MERCHANT_contract_free (struct TALER_MERCHANT_Contract *contract); + #endif diff --git a/src/lib/merchant_api_common.c b/src/lib/merchant_api_common.c @@ -38,7 +38,7 @@ TALER_MERCHANT_format_fractional_string (uint64_t integer, { GNUNET_assert (NULL != buffer); GNUNET_assert (0 < buffer_length); - GNUNET_assert (fractional < MERCHANT_UNIT_FRAC_BASE); + GNUNET_assert (fractional < TALER_MERCHANT_UNIT_FRAC_BASE); if (0 == fractional) { @@ -49,13 +49,13 @@ TALER_MERCHANT_format_fractional_string (uint64_t integer, return; } { - char frac_buf[MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; + char frac_buf[TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; size_t idx; GNUNET_snprintf (frac_buf, sizeof (frac_buf), "%0*u", - MERCHANT_UNIT_FRAC_MAX_DIGITS, + TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS, (unsigned int) fractional); for (idx = strlen (frac_buf); idx > 0; idx--) { diff --git a/src/lib/merchant_api_common.h b/src/lib/merchant_api_common.h @@ -67,10 +67,10 @@ TALER_MERCHANT_parse_fractional_string (bool allow_negative, /** * Format a quantity into its decimal string representation using the merchant - * fixed-point base (MERCHANT_UNIT_FRAC_BASE). + * fixed-point base (TALER_MERCHANT_UNIT_FRAC_BASE). * * @param quantity integer part - * @param quantity_frac fractional part (0..MERCHANT_UNIT_FRAC_BASE-1) + * @param quantity_frac fractional part (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) * @param[out] buffer output buffer * @param buffer_length size of @a buffer */ @@ -83,10 +83,10 @@ TALER_MERCHANT_format_quantity_string (uint64_t quantity, /** * Format a stock value into its decimal string representation using the - * merchant fixed-point base (MERCHANT_UNIT_FRAC_BASE). + * merchant fixed-point base (TALER_MERCHANT_UNIT_FRAC_BASE). * * @param total_stock integer part - * @param total_stock_frac fractional part (0..MERCHANT_UNIT_FRAC_BASE-1) + * @param total_stock_frac fractional part (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) * @param[out] buffer output buffer * @param buffer_length size of @a buffer */ diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -49,7 +49,7 @@ * commands should NOT wait for this timeout! */ #define POLL_ORDER_TIMEOUT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 60) + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 60) /** * The 'poll-orders-conclude-1x' and other 'conclude' @@ -57,7 +57,7 @@ * here we use a short value! */ #define POLL_ORDER_SHORT_TIMEOUT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 2) + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 2) /** * Configuration file we use. One (big) configuration is used @@ -193,8 +193,8 @@ cmd_exec_wirewatch (const char *label) * @param label label to use for the command. */ #define CMD_EXEC_AGGREGATOR(label) \ - TALER_TESTING_cmd_exec_aggregator (label "-aggregator", config_file), \ - TALER_TESTING_cmd_exec_transfer (label "-transfer", config_file) + TALER_TESTING_cmd_exec_aggregator (label "-aggregator", config_file), \ + TALER_TESTING_cmd_exec_transfer (label "-transfer", config_file) /** @@ -657,7 +657,8 @@ run (void *cls, "", json_array (), 1, - (MERCHANT_UNIT_FRAC_BASE * 3) + (TALER_MERCHANT_UNIT_FRAC_BASE + * 3) / 4, true, 0, @@ -697,11 +698,13 @@ run (void *cls, "", json_array (), 2, - MERCHANT_UNIT_FRAC_BASE / 4, + TALER_MERCHANT_UNIT_FRAC_BASE / 4 + , true, 0, json_object (), - GNUNET_TIME_relative_to_timestamp ( + GNUNET_TIME_relative_to_timestamp + ( GNUNET_TIME_UNIT_MINUTES), MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_merchant_get_product ("get-product-frac-patched", @@ -846,7 +849,7 @@ run (void *cls, "product-3", GNUNET_TIME_UNIT_MINUTES, 1, - MERCHANT_UNIT_FRAC_BASE / 2, + TALER_MERCHANT_UNIT_FRAC_BASE / 2, true, MHD_HTTP_BAD_REQUEST), TALER_TESTING_cmd_merchant_post_orders2 ("create-proposal-p3-float-denied", @@ -901,7 +904,7 @@ run (void *cls, "product-3", GNUNET_TIME_UNIT_MINUTES, 1, - MERCHANT_UNIT_FRAC_BASE / 2, + TALER_MERCHANT_UNIT_FRAC_BASE / 2, true, MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_merchant_post_orders2 ("create-proposal-p3-float", diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh @@ -523,20 +523,22 @@ NOW=$(date +%s) TO_SLEEP=$((1200 + WIRE_DEADLINE - NOW )) echo "Waiting $TO_SLEEP secs for wire transfer" -echo -n "Perform wire transfers ..." +echo -n "Call taler-exchange-aggregator ..." taler-exchange-aggregator \ -y \ -c "$CONF" \ -T "${TO_SLEEP}"000000 \ -t \ -L INFO &> aggregator.log +echo " DONE" +echo -n "Call taler-exchange-transfer ..." taler-exchange-transfer \ -c "$CONF" \ -t \ -L INFO &> transfer.log echo " DONE" echo -n "Give time to Nexus to route the payment to Sandbox..." -# FIXME-MS: trigger immediate update at nexus +# FIXME: trigger immediate update at nexus # NOTE: once libeufin can do long-polling, we should # be able to reduce the delay here and run aggregator/transfer # always in the background via setup diff --git a/src/testing/testing_api_cmd_lock_product.c b/src/testing/testing_api_cmd_lock_product.c @@ -70,7 +70,7 @@ struct LockProductState uint32_t quantity; /** - * Fractional component of the quantity (units of 1/MERCHANT_UNIT_FRAC_BASE) when + * Fractional component of the quantity (units of 1/TALER_MERCHANT_UNIT_FRAC_BASE) when * @e use_fractional_quantity is true. */ uint32_t quantity_frac; diff --git a/src/testing/testing_api_cmd_post_orders.c b/src/testing/testing_api_cmd_post_orders.c @@ -539,16 +539,16 @@ orders_run2 (void *cls, GNUNET_break (0); break; } - scaled = llround (frac * (double) MERCHANT_UNIT_FRAC_BASE); + scaled = llround (frac * (double) TALER_MERCHANT_UNIT_FRAC_BASE); if (scaled < 0) { GNUNET_break (0); break; } - if (scaled >= (long long) MERCHANT_UNIT_FRAC_BASE) + if (scaled >= (long long) TALER_MERCHANT_UNIT_FRAC_BASE) { quantity_int++; - scaled -= MERCHANT_UNIT_FRAC_BASE; + scaled -= TALER_MERCHANT_UNIT_FRAC_BASE; } quantity_frac_local = (uint32_t) scaled; pd.quantity = quantity_int; diff --git a/src/util/Makefile.am b/src/util/Makefile.am @@ -43,7 +43,9 @@ libtalermerchantutil_la_SOURCES = \ contract_serialize.c \ json.c \ mfa.c \ - os_installation.c + os_installation.c \ + value_kinds.c \ + validators.c libtalermerchantutil_la_LIBADD = \ -lgnunetjson \ -lgnunetutil \ diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c @@ -1107,12 +1107,106 @@ parse_contract_v1 ( } +/** + * Parse the given unit quantity string @a s and store the result in @a q. + * + * @param s quantity to parse + * @param[out] q where to store the result + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_unit_quantity (const char *s, + struct TALER_MERCHANT_ProductQuantity *q) +{ + const char *ptr; + uint64_t integer = 0; + uint32_t frac = 0; + unsigned int digits = 0; + + if (NULL == s) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + ptr = s; + if ('\0' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ('-' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (! isdigit ((unsigned char) *ptr)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + while (isdigit ((unsigned char) *ptr)) + { + unsigned int digit = (unsigned int) (*ptr - '0'); + + /* We intentionally allow at most INT64_MAX (as -1 has special meanings), + even though the data type would support UINT64_MAX */ + if (integer > (INT64_MAX - digit) / 10) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + integer = integer * 10 + digit; + ptr++; + } + if ('.' == *ptr) + { + ptr++; + if ('\0' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + while (isdigit ((unsigned char) *ptr)) + { + unsigned int digit = (unsigned int) (*ptr - '0'); + + if (digits >= TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + frac = (uint32_t) (frac * 10 + digit); + digits++; + ptr++; + } + while (digits < TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + { + frac *= 10; + digits++; + } + } + if ('\0' != *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + q->integer = integer; + q->fractional = frac; + return GNUNET_OK; +} + + enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_product (const json_t *p, - struct TALER_MERCHANT_Product *r) +TALER_MERCHANT_parse_product_sold (const json_t *pj, + struct TALER_MERCHANT_ProductSold *r) { - bool have_quantity; - bool have_unit_quantity; + bool no_quantity; + bool no_unit_quantity; + bool no_price; + uint64_t legacy_quantity; + const char *unit_quantity_s; + struct TALER_Amount price; + const json_t *prices = NULL; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string_copy ("product_id", @@ -1132,19 +1226,23 @@ TALER_MERCHANT_parse_product (const json_t *p, NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint64 ("quantity", - &r->quantity), - &have_quantity), + &legacy_quantity), + &no_quantity), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("unit_quantity", - &r->unit_quantity), - &have_unit_quantity), + GNUNET_JSON_spec_string ("unit_quantity", + &unit_quantity_s), + &no_unit_quantity), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string_copy ("unit", &r->unit), NULL), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount_any ("price", - &r->price), + &price), + &no_price), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("prices", + &prices), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string_copy ("image", @@ -1169,7 +1267,7 @@ TALER_MERCHANT_parse_product (const json_t *p, unsigned int eline; r->delivery_date = GNUNET_TIME_UNIT_FOREVER_TS; - res = GNUNET_JSON_parse (p, + res = GNUNET_JSON_parse (pj, spec, &ename, &eline); @@ -1181,10 +1279,62 @@ TALER_MERCHANT_parse_product (const json_t *p, ename); return GNUNET_SYSERR; } - if (have_quantity && have_unit_quantity) + if (! no_quantity) { - GNUNET_break (0); - r->quantity = 0; + r->unit_quantity.integer = legacy_quantity; + r->unit_quantity.fractional = 0; + } + if (! no_unit_quantity) + { + if (GNUNET_OK != + parse_unit_quantity (unit_quantity_s, + &r->unit_quantity)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + if ( (! no_quantity) && (! no_unit_quantity) ) + { + GNUNET_break ( (0 == r->unit_quantity.fractional) && + (legacy_quantity == r->unit_quantity.integer) ); + } + if (NULL != prices) + { + size_t len = json_array_size (prices); + size_t i; + json_t *price_i; + + GNUNET_assert (len < UINT_MAX); + r->prices = GNUNET_new_array ((unsigned int) len, + struct TALER_Amount); + json_array_foreach (prices, i, price_i) + { + struct GNUNET_JSON_Specification pspec[] = { + TALER_JSON_spec_amount_any (NULL, + &r->prices[i]), + GNUNET_JSON_spec_end () + }; + + res = GNUNET_JSON_parse (price_i, + pspec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse price at index %u\n", + (unsigned int) i); + return GNUNET_SYSERR; + } + } + } + else if (! no_price) + { + r->prices = GNUNET_new_array (1, + struct TALER_Amount); + r->prices[0] = price; } return GNUNET_OK; } @@ -1309,12 +1459,12 @@ TALER_MERCHANT_contract_parse (json_t *input, json_t *p; contract->products = GNUNET_new_array (contract->products_len, - struct TALER_MERCHANT_Product); + struct TALER_MERCHANT_ProductSold); json_array_foreach (products, i, p) { if (GNUNET_OK != - TALER_MERCHANT_parse_product (p, - &contract->products[i])) + TALER_MERCHANT_parse_product_sold (p, + &contract->products[i])) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, @@ -1353,13 +1503,13 @@ cleanup: void -TALER_MERCHANT_product_free (struct TALER_MERCHANT_Product *product) +TALER_MERCHANT_product_sold_free (struct TALER_MERCHANT_ProductSold *product) { GNUNET_free (product->product_id); GNUNET_free (product->product_name); GNUNET_free (product->description); json_decref (product->description_i18n); - GNUNET_free (product->unit_quantity); + GNUNET_free (product->prices); GNUNET_free (product->unit); GNUNET_free (product->image); json_decref (product->taxes); @@ -1400,7 +1550,7 @@ TALER_MERCHANT_contract_free ( if (NULL != contract->products) { for (size_t i = 0; i<contract->products_len; i++) - TALER_MERCHANT_product_free (&contract->products[i]); + TALER_MERCHANT_product_sold_free (&contract->products[i]); GNUNET_free (contract->products); } GNUNET_free (contract->wire_method); diff --git a/src/util/contract_serialize.c b/src/util/contract_serialize.c @@ -385,10 +385,39 @@ json_from_contract_v1 ( } +/** + * Convert quantity @a q into a string for JSON serialization + * + * @param q quantity to convert + * @return formatted string + */ +static const char * +quantity_to_string (const struct TALER_MERCHANT_ProductQuantity *q) +{ + static char res[64]; + + TALER_MERCHANT_vk_format_fractional_string (TALER_MERCHANT_VK_QUANTITY, + q->integer, + q->fractional, + sizeof (res), + res); + return res; +} + + json_t * -TALER_MERCHANT_product_serialize ( - const struct TALER_MERCHANT_Product *p) +TALER_MERCHANT_product_sold_serialize ( + const struct TALER_MERCHANT_ProductSold *p) { + json_t *prices; + + prices = json_array (); + GNUNET_assert (NULL != prices); + for (unsigned int i = 0; i<p->prices_length; i++) + GNUNET_assert (0 == + json_array_append_new (prices, + TALER_JSON_from_amount ( + &p->prices[i]))); return GNUNET_JSON_PACK ( GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("product_id", @@ -403,22 +432,30 @@ TALER_MERCHANT_product_serialize ( GNUNET_JSON_pack_object_steal ("description_i18n", p->description_i18n)), GNUNET_JSON_pack_allow_null ( - (NULL != p->unit_quantity) + ( (0 != p->unit_quantity.integer) || + (0 != p->unit_quantity.fractional) ) ? GNUNET_JSON_pack_string ("unit_quantity", - p->unit_quantity) - : ( (0 != p->quantity) - ? GNUNET_JSON_pack_uint64 ("quantity", - p->quantity) - : GNUNET_JSON_pack_string ("dummy", - NULL) ) ), + quantity_to_string (&p->unit_quantity)) + : GNUNET_JSON_pack_string ("dummy", + NULL) ), + /* Legacy */ + GNUNET_JSON_pack_allow_null ( + (0 == p->unit_quantity.fractional) + ? GNUNET_JSON_pack_uint64 ("quantity", + p->unit_quantity.integer) + : GNUNET_JSON_pack_string ("dummy", + NULL) ), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("unit", p->unit)), + /* Deprecated, use prices! */ GNUNET_JSON_pack_allow_null ( TALER_JSON_pack_amount ("price", - TALER_amount_is_valid (&p->price) - ? &p->price + TALER_amount_is_valid (&p->prices[0]) + ? &p->prices[0] : NULL)), + GNUNET_JSON_pack_array_steal ("prices", + prices), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("image", p->image)), @@ -468,7 +505,7 @@ success: GNUNET_assert ( 0 == json_array_append_new (products, - TALER_MERCHANT_product_serialize ( + TALER_MERCHANT_product_sold_serialize ( &input->products[i]))); } diff --git a/src/util/validators.c b/src/util/validators.c @@ -0,0 +1,53 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, 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 validators.c + * @brief Input validators + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_db_lib.h> +#include <taler/taler_json_lib.h> +#include "taler_merchant_util.h" + + +bool +TALER_MERCHANT_image_data_url_valid (const char *image_data_url) +{ + if (0 == strcmp (image_data_url, + "")) + return true; + if (0 != strncasecmp ("data:image/", + image_data_url, + strlen ("data:image/"))) + { + GNUNET_break_op (0); + return false; + } + if (NULL == strstr (image_data_url, + ";base64,")) + { + GNUNET_break_op (0); + return false; + } + if (! TALER_url_valid_charset (image_data_url)) + { + GNUNET_break_op (0); + return false; + } + return true; +} diff --git a/src/util/value_kinds.c b/src/util/value_kinds.c @@ -0,0 +1,317 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, 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 value_kinds.c + * @brief Parsing quantities and other decimal fractions + * @author Christian Grothoff + * @author Bohdan + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_db_lib.h> +#include <taler/taler_json_lib.h> +#include "taler_merchant_util.h" + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_vk_parse_fractional_string ( + const char *value, + int64_t *integer_part, + uint32_t *fractional_part) +{ + const char *ptr; + uint64_t integer = 0; + uint32_t frac = 0; + unsigned int digits = 0; + + GNUNET_assert (NULL != integer_part); + GNUNET_assert (NULL != fractional_part); + + if (NULL == value) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + ptr = value; + if ('\0' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ('-' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (! isdigit ((unsigned char) *ptr)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + while (isdigit ((unsigned char) *ptr)) + { + unsigned int digit = (unsigned int) (*ptr - '0'); + + if (integer > (UINT64_MAX - digit) / 10) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + integer = integer * 10 + digit; + ptr++; + } + if ('.' == *ptr) + { + ptr++; + if ('\0' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + while (isdigit ((unsigned char) *ptr)) + { + unsigned int digit = (unsigned int) (*ptr - '0'); + + if (digits >= TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + frac = (uint32_t) (frac * 10 + digit); + digits++; + ptr++; + } + while (digits < TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + { + frac *= 10; + digits++; + } + } + if ('\0' != *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (integer > (uint64_t) INT64_MAX) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + *integer_part = integer; + *fractional_part = frac; + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_vk_process_quantity_inputs (enum TALER_MERCHANT_ValueKind kind, + bool allow_fractional, + bool int_missing, + int64_t int_raw, + bool str_missing, + const char *str_raw, + uint64_t *int_out, + uint32_t *frac_out, + const char **error_param) +{ + static char errbuf[128]; + int64_t parsed_int = 0; + uint32_t parsed_frac = 0; + const char *int_field = (TALER_MERCHANT_VK_STOCK == kind) + ? "total_stock" + : "quantity"; + const char *str_field = (TALER_MERCHANT_VK_STOCK == kind) + ? "unit_total_stock" + : "unit_quantity"; + + *error_param = NULL; + + if (int_missing && str_missing) + { + GNUNET_snprintf (errbuf, + sizeof (errbuf), + "missing %s and %s", + int_field, + str_field); + *error_param = errbuf; + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (! str_missing) + { + if ( (TALER_MERCHANT_VK_STOCK == kind) && + (0 == strcmp ("-1", + str_raw)) ) + { + parsed_int = -1; + parsed_frac = 0; + } + else + { + if (GNUNET_OK != + TALER_MERCHANT_vk_parse_fractional_string (str_raw, + &parsed_int, + &parsed_frac)) + { + GNUNET_snprintf (errbuf, + sizeof (errbuf), + "malformed %s", + str_field); + *error_param = errbuf; + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + } + + if ( (! int_missing) && (! str_missing) ) + { + if ( (parsed_int != int_raw) || (0 != parsed_frac) ) + { + GNUNET_snprintf (errbuf, + sizeof (errbuf), + "%s/%s mismatch", + int_field, + str_field); + *error_param = errbuf; + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + else if (int_missing) + { + int_raw = parsed_int; + } + + if ( (TALER_MERCHANT_VK_STOCK == kind) && (-1 == int_raw) ) + { + if ( (! str_missing) && (0 != parsed_frac) ) + { + GNUNET_snprintf (errbuf, + sizeof (errbuf), + "fractional part forbidden with %s='-1'", + str_field); + *error_param = errbuf; + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + *int_out = INT64_MAX; + *frac_out = INT32_MAX; + return GNUNET_OK; + } + + if (int_raw < 0) + { + GNUNET_snprintf (errbuf, + sizeof (errbuf), + "%s must be non-negative", + int_field); + *error_param = errbuf; + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (! allow_fractional) + { + if ( (! str_missing) && (0 != parsed_frac) ) + { + GNUNET_snprintf (errbuf, + sizeof (errbuf), + "fractional part not allowed for %s", + str_field); + *error_param = errbuf; + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + parsed_frac = 0; + } + else if (! str_missing) + { + if (parsed_frac >= TALER_MERCHANT_UNIT_FRAC_BASE) + { + GNUNET_snprintf (errbuf, + sizeof (errbuf), + "%s fractional part exceeds base %u", + str_field, + TALER_MERCHANT_UNIT_FRAC_BASE); + *error_param = errbuf; + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + + *int_out = (uint64_t) int_raw; + *frac_out = parsed_frac; + return GNUNET_OK; +} + + +void +TALER_MERCHANT_vk_format_fractional_string ( + enum TALER_MERCHANT_ValueKind kind, + uint64_t integer, + uint32_t fractional, + size_t buffer_length, + char buffer[static buffer_length]) +{ + GNUNET_assert (0 < buffer_length); + + if ( (TALER_MERCHANT_VK_STOCK == kind) && + (INT64_MAX == (int64_t) integer) && + (INT32_MAX == (int32_t) fractional) ) + { + GNUNET_snprintf (buffer, + buffer_length, + "-1"); + return; + } + + GNUNET_assert ( (TALER_MERCHANT_VK_QUANTITY != kind) || + ((INT64_MAX != (int64_t) integer) && + (INT32_MAX != (int32_t) fractional)) ); + GNUNET_assert (fractional < TALER_MERCHANT_UNIT_FRAC_BASE); + + if (0 == fractional) + { + GNUNET_snprintf (buffer, + buffer_length, + "%lu", + integer); + return; + } + { + char frac_buf[TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; + size_t idx; + + GNUNET_snprintf (frac_buf, + sizeof (frac_buf), + "%0*u", + TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS, + (unsigned int) fractional); + for (idx = strlen (frac_buf); idx > 0; idx--) + { + if ('0' != frac_buf[idx - 1]) + break; + frac_buf[idx - 1] = '\0'; + } + GNUNET_snprintf (buffer, + buffer_length, + "%lu.%s", + integer, + frac_buf); + } +}