template_parse.c (15381B)
1 /* 2 This file is part of TALER 3 (C) 2025 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Lesser General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file src/util/template_parse.c 18 * @brief shared logic for template contract parsing 19 * @author Bohdan Potuzhnyi 20 */ 21 #include "platform.h" 22 #include <gnunet/gnunet_common.h> 23 #include <gnunet/gnunet_json_lib.h> 24 #include <jansson.h> 25 #include <string.h> 26 #include <taler/taler_json_lib.h> 27 #include <taler/taler_util.h> 28 #include "taler/taler_merchant_util.h" 29 #include <regex.h> 30 31 32 enum TALER_MERCHANT_TemplateType 33 TALER_MERCHANT_template_type_from_contract (const json_t *template_contract) 34 { 35 const json_t *type_val; 36 37 if (NULL == template_contract) 38 return TALER_MERCHANT_TEMPLATE_TYPE_INVALID; 39 40 type_val = json_object_get (template_contract, 41 "template_type"); 42 43 if (NULL == type_val) 44 { 45 /* missing => fixed order */ 46 return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER; 47 } 48 if (! json_is_string (type_val)) 49 { 50 GNUNET_break_op (0); 51 return TALER_MERCHANT_TEMPLATE_TYPE_INVALID; 52 } 53 54 return TALER_MERCHANT_template_type_from_string ( 55 json_string_value (type_val)); 56 } 57 58 59 enum TALER_MERCHANT_TemplateType 60 TALER_MERCHANT_template_type_from_string (const char *template_type) 61 { 62 if (NULL == template_type) 63 return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER; 64 if (0 == strcmp (template_type, 65 "fixed-order")) 66 return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER; 67 if (0 == strcmp (template_type, 68 "inventory-cart")) 69 return TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART; 70 if (0 == strcmp (template_type, 71 "paivana")) 72 return TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA; 73 return TALER_MERCHANT_TEMPLATE_TYPE_INVALID; 74 } 75 76 77 const char * 78 TALER_MERCHANT_template_type_to_string ( 79 enum TALER_MERCHANT_TemplateType template_type) 80 { 81 switch (template_type) 82 { 83 case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: 84 return "fixed-order"; 85 case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: 86 return "inventory-cart"; 87 case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: 88 return "paivana"; 89 case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: 90 break; 91 } 92 return NULL; 93 } 94 95 96 /** 97 * Parse inventory-specific fields from a template contract. 98 * 99 * @param template_contract json 100 * @param[out] out where to write parsed fields 101 * @param[out] error_name error description 102 * @return #GNUNET_OK on success, #GNUNET_SYSERR on parse/validation failure 103 */ 104 static enum GNUNET_GenericReturnValue 105 parse_template_inventory (const json_t *template_contract, 106 struct TALER_MERCHANT_TemplateContract *out, 107 const char **error_name) 108 { 109 struct GNUNET_JSON_Specification spec[] = { 110 GNUNET_JSON_spec_mark_optional ( 111 GNUNET_JSON_spec_bool ( 112 "selected_all", 113 &out->details.inventory.selected_all), 114 NULL), 115 GNUNET_JSON_spec_mark_optional ( 116 GNUNET_JSON_spec_array_const ( 117 "selected_categories", 118 &out->details.inventory.selected_categories) 119 , 120 NULL), 121 GNUNET_JSON_spec_mark_optional ( 122 GNUNET_JSON_spec_array_const ( 123 "selected_products", 124 &out->details.inventory.selected_products), 125 NULL), 126 GNUNET_JSON_spec_mark_optional ( 127 GNUNET_JSON_spec_bool ( 128 "choose_one", 129 &out->details.inventory.choose_one), 130 NULL), 131 GNUNET_JSON_spec_end () 132 }; 133 const char *en; 134 135 if (GNUNET_OK != 136 GNUNET_JSON_parse ((json_t *) template_contract, 137 spec, 138 &en, 139 NULL)) 140 { 141 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 142 "Invalid inventory template_contract for field %s\n", 143 en); 144 if (NULL != error_name) 145 *error_name = en; 146 return GNUNET_SYSERR; 147 } 148 149 if (NULL != out->details.inventory.selected_categories) 150 { 151 const json_t *entry; 152 size_t idx; 153 154 json_array_foreach ((json_t *) out->details.inventory.selected_categories, 155 idx, 156 entry) 157 { 158 if ( (! json_is_integer (entry)) || 159 (0 > json_integer_value (entry)) ) 160 { 161 GNUNET_break_op (0); 162 if (NULL != error_name) 163 *error_name = "selected_categories"; 164 return GNUNET_SYSERR; 165 } 166 } 167 } 168 169 if (NULL != out->details.inventory.selected_products) 170 { 171 const json_t *entry; 172 size_t idx; 173 174 json_array_foreach ((json_t *) out->details.inventory.selected_products, 175 idx, 176 entry) 177 { 178 if (! json_is_string (entry)) 179 { 180 GNUNET_break_op (0); 181 if (NULL != error_name) 182 *error_name = "selected_products"; 183 return GNUNET_SYSERR; 184 } 185 } 186 } 187 return GNUNET_OK; 188 } 189 190 191 /** 192 * Parse paivana-specific fields from a template contract. 193 * 194 * @param template_contract json 195 * @param[out] out where to write parsed fields 196 * @param[out] error_name error description 197 * @return #GNUNET_OK on success, #GNUNET_SYSERR on parse/validation failure 198 */ 199 static enum GNUNET_GenericReturnValue 200 parse_template_paivana (const json_t *template_contract, 201 struct TALER_MERCHANT_TemplateContract *out, 202 const char **error_name) 203 { 204 struct GNUNET_JSON_Specification spec[] = { 205 GNUNET_JSON_spec_mark_optional ( 206 GNUNET_JSON_spec_string ("website_regex", 207 &out->details.paivana.website_regex), 208 NULL), 209 TALER_MERCHANT_spec_order_choices ("choices", 210 &out->details.paivana.choices, 211 &out->details.paivana.choices_len), 212 GNUNET_JSON_spec_end () 213 }; 214 const char *en; 215 216 if (GNUNET_OK != 217 GNUNET_JSON_parse ((json_t *) template_contract, 218 spec, 219 &en, 220 NULL)) 221 { 222 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 223 "Invalid paivana template_contract for field %s\n", 224 en); 225 if (NULL != error_name) 226 *error_name = en; 227 return GNUNET_SYSERR; 228 } 229 if (NULL != out->details.paivana.website_regex) 230 { 231 regex_t ex; 232 233 if (0 != regcomp (&ex, 234 out->details.paivana.website_regex, 235 REG_NOSUB | REG_EXTENDED)) 236 { 237 GNUNET_break_op (0); 238 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 239 "Invalid paivana website_regex given\n"); 240 if (NULL != error_name) 241 *error_name = "Invalid website_regex given"; 242 return GNUNET_SYSERR; 243 } 244 regfree (&ex); 245 } 246 return GNUNET_OK; 247 } 248 249 250 /** 251 * Check that the (optional) @e min_amount and @e max_amount of @a tc are 252 * usable: they must be denominated in the same currency, which must be 253 * the currency of the template (which then must be given, as otherwise we 254 * could not tell in which currency to enforce the limits), and they must 255 * not exclude each other. 256 * 257 * @param tc template contract to check 258 * @param[out] error_name error description 259 * @return #GNUNET_OK on success, #GNUNET_SYSERR on validation failure 260 */ 261 static enum GNUNET_GenericReturnValue 262 check_amount_limits (const struct TALER_MERCHANT_TemplateContract *tc, 263 const char **error_name) 264 { 265 if (tc->no_min_amount && 266 tc->no_max_amount) 267 return GNUNET_OK; 268 if ( (! tc->no_min_amount) && 269 (! tc->no_max_amount) ) 270 { 271 /* Must be checked before the two are compared, as comparing 272 amounts of different currencies fails an assertion. Note that 273 we deliberately do not rely on both having been checked against 274 the currency of the template here. */ 275 if (GNUNET_YES != 276 TALER_amount_cmp_currency (&tc->min_amount, 277 &tc->max_amount)) 278 { 279 GNUNET_break_op (0); 280 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 281 "min_amount and max_amount must use the same currency\n"); 282 if (NULL != error_name) 283 *error_name = "max_amount"; 284 return GNUNET_SYSERR; 285 } 286 if (0 < TALER_amount_cmp (&tc->min_amount, 287 &tc->max_amount)) 288 { 289 GNUNET_break_op (0); 290 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 291 "min_amount exceeds max_amount\n"); 292 if (NULL != error_name) 293 *error_name = "min_amount"; 294 return GNUNET_SYSERR; 295 } 296 } 297 if (NULL == tc->currency) 298 { 299 GNUNET_break_op (0); 300 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 301 "Template with min_amount or max_amount must specify a currency\n"); 302 if (NULL != error_name) 303 *error_name = "currency"; 304 return GNUNET_SYSERR; 305 } 306 if ( (! tc->no_min_amount) && 307 (0 != strcasecmp (tc->currency, 308 tc->min_amount.currency)) ) 309 { 310 GNUNET_break_op (0); 311 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 312 "min_amount must use the currency of the template (%s)\n", 313 tc->currency); 314 if (NULL != error_name) 315 *error_name = "min_amount"; 316 return GNUNET_SYSERR; 317 } 318 if ( (! tc->no_max_amount) && 319 (0 != strcasecmp (tc->currency, 320 tc->max_amount.currency)) ) 321 { 322 GNUNET_break_op (0); 323 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 324 "max_amount must use the currency of the template (%s)\n", 325 tc->currency); 326 if (NULL != error_name) 327 *error_name = "max_amount"; 328 return GNUNET_SYSERR; 329 } 330 return GNUNET_OK; 331 } 332 333 334 enum GNUNET_GenericReturnValue 335 TALER_MERCHANT_template_contract_parse ( 336 const json_t *template_contract, 337 struct TALER_MERCHANT_TemplateContract *out, 338 const char **error_name) 339 { 340 const char *template_type_str = NULL; 341 struct GNUNET_JSON_Specification spec[] = { 342 GNUNET_JSON_spec_mark_optional ( 343 GNUNET_JSON_spec_string ("template_type", 344 &template_type_str), 345 NULL), 346 GNUNET_JSON_spec_mark_optional ( 347 GNUNET_JSON_spec_string ("summary", 348 &out->summary), 349 NULL), 350 GNUNET_JSON_spec_mark_optional ( 351 GNUNET_JSON_spec_string ("currency", 352 &out->currency), 353 NULL), 354 GNUNET_JSON_spec_mark_optional ( 355 TALER_JSON_spec_amount_any ("amount", 356 &out->amount), 357 &out->no_amount), 358 GNUNET_JSON_spec_mark_optional ( 359 TALER_JSON_spec_amount_any ("min_amount", 360 &out->min_amount), 361 &out->no_min_amount), 362 GNUNET_JSON_spec_mark_optional ( 363 TALER_JSON_spec_amount_any ("max_amount", 364 &out->max_amount), 365 &out->no_max_amount), 366 GNUNET_JSON_spec_mark_optional ( 367 GNUNET_JSON_spec_uint32 ("minimum_age", 368 &out->minimum_age), 369 NULL), 370 GNUNET_JSON_spec_mark_optional ( 371 GNUNET_JSON_spec_relative_time ("pay_duration", 372 &out->pay_duration), 373 NULL), 374 GNUNET_JSON_spec_mark_optional ( 375 GNUNET_JSON_spec_relative_time ("max_pickup_duration", 376 &out->max_pickup_duration), 377 NULL), 378 GNUNET_JSON_spec_mark_optional ( 379 GNUNET_JSON_spec_bool ("request_tip", 380 &out->request_tip), 381 NULL), 382 GNUNET_JSON_spec_end () 383 }; 384 const char *en; 385 386 if (NULL == template_contract) 387 { 388 if (NULL != error_name) 389 *error_name = "template_contract is NULL"; 390 return GNUNET_SYSERR; 391 } 392 out->max_pickup_duration = GNUNET_TIME_UNIT_FOREVER_REL; 393 out->no_min_amount = true; 394 out->no_max_amount = true; 395 if (GNUNET_OK != 396 GNUNET_JSON_parse ((json_t *) template_contract, 397 spec, 398 &en, 399 NULL)) 400 { 401 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 402 "Invalid input for field %s\n", 403 en); 404 if (NULL != error_name) 405 *error_name = en; 406 return GNUNET_SYSERR; 407 } 408 if (out->minimum_age > UINT8_MAX) 409 { 410 /* Orders only support ages up to UINT8_MAX, see the "minimum_age" 411 field of the contract base terms. */ 412 GNUNET_break_op (0); 413 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 414 "Invalid minimum_age %u in template\n", 415 (unsigned int) out->minimum_age); 416 if (NULL != error_name) 417 *error_name = "minimum_age"; 418 return GNUNET_SYSERR; 419 } 420 if (GNUNET_OK != 421 check_amount_limits (out, 422 error_name)) 423 return GNUNET_SYSERR; 424 425 out->type = TALER_MERCHANT_template_type_from_string (template_type_str); 426 if (TALER_MERCHANT_TEMPLATE_TYPE_INVALID == out->type) 427 { 428 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 429 "Invalid template_type used '%s'\n", 430 template_type_str); 431 if (NULL != error_name) 432 *error_name = "Invalid template_type used"; 433 return GNUNET_SYSERR; 434 } 435 436 /* Parse additional fields for each specific type */ 437 switch (out->type) 438 { 439 case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: 440 return GNUNET_OK; 441 case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: 442 return parse_template_inventory (template_contract, 443 out, 444 error_name); 445 case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: 446 return parse_template_paivana (template_contract, 447 out, 448 error_name); 449 case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: 450 break; 451 } 452 453 /* I think we are never supposed to reach it */ 454 GNUNET_break_op (0); 455 if (NULL != error_name) 456 *error_name = "template_type"; 457 return GNUNET_SYSERR; 458 } 459 460 461 void 462 TALER_MERCHANT_template_contract_free ( 463 struct TALER_MERCHANT_TemplateContract *tc) 464 { 465 switch (tc->type) 466 { 467 case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: 468 return; 469 case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: 470 return; 471 case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: 472 for (unsigned int i = 0; i<tc->details.paivana.choices_len; i++) 473 TALER_MERCHANT_order_choice_free (&tc->details.paivana.choices[i]); 474 GNUNET_array_grow (tc->details.paivana.choices, 475 tc->details.paivana.choices_len, 476 0); 477 return; 478 case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: 479 return; 480 } 481 } 482 483 484 bool 485 TALER_MERCHANT_template_contract_valid (const json_t *template_contract) 486 { 487 struct TALER_MERCHANT_TemplateContract tmp; 488 bool ret; 489 490 memset (&tmp, 491 0, 492 sizeof (tmp)); 493 ret = (GNUNET_OK == 494 TALER_MERCHANT_template_contract_parse (template_contract, 495 &tmp, 496 NULL)); 497 if (ret && 498 GNUNET_TIME_relative_is_forever (tmp.pay_duration)) 499 ret = false; 500 TALER_MERCHANT_template_contract_free (&tmp); 501 return ret; 502 }