diff options
Diffstat (limited to 'src/kyclogic/kyclogic_api.c')
-rw-r--r-- | src/kyclogic/kyclogic_api.c | 522 |
1 files changed, 486 insertions, 36 deletions
diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c index f2d31acf5..186799dbb 100644 --- a/src/kyclogic/kyclogic_api.c +++ b/src/kyclogic/kyclogic_api.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022-2023 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -22,6 +22,12 @@ #include "taler_kyclogic_lib.h" /** + * Name of the KYC check that may never be passed. Useful if some + * operations/amounts are categorically forbidden. + */ +#define KYC_CHECK_IMPOSSIBLE "impossible" + +/** * Information about a KYC provider. */ struct TALER_KYCLOGIC_KycProvider; @@ -180,9 +186,11 @@ TALER_KYCLOGIC_kyc_trigger_from_string (const char *trigger_s, enum TALER_KYCLOGIC_KycTriggerEvent out; } map [] = { { "withdraw", TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW }, + { "age-withdraw", TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW }, { "deposit", TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT }, { "merge", TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE }, { "balance", TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE }, + { "close", TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE }, { NULL, 0 } }; @@ -207,12 +215,16 @@ TALER_KYCLOGIC_kyc_trigger2s (enum TALER_KYCLOGIC_KycTriggerEvent trigger) { case TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW: return "withdraw"; + case TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW: + return "age-withdraw"; case TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT: return "deposit"; case TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE: return "merge"; case TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE: return "balance"; + case TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE: + return "close"; } GNUNET_break (0); return NULL; @@ -226,7 +238,7 @@ TALER_KYCLOGIC_kyc_user_type_from_string (const char *ut_s, struct { const char *in; - enum TALER_KYCLOGIC_KycTriggerEvent out; + enum TALER_KYCLOGIC_KycUserType out; } map [] = { { "individual", TALER_KYCLOGIC_KYC_UT_INDIVIDUAL }, { "business", TALER_KYCLOGIC_KYC_UT_BUSINESS }, @@ -262,6 +274,38 @@ TALER_KYCLOGIC_kyc_user_type2s (enum TALER_KYCLOGIC_KycUserType ut) } +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_check_satisfiable ( + const char *check_name) +{ + for (unsigned int i = 0; i<num_kyc_checks; i++) + if (0 == strcmp (check_name, + kyc_checks[i]->name)) + return GNUNET_OK; + if (0 == strcmp (check_name, + KYC_CHECK_IMPOSSIBLE)) + return GNUNET_NO; + return GNUNET_SYSERR; +} + + +json_t * +TALER_KYCLOGIC_get_satisfiable () +{ + json_t *requirements; + + requirements = json_array (); + GNUNET_assert (NULL != requirements); + for (unsigned int i = 0; i<num_kyc_checks; i++) + GNUNET_assert ( + 0 == + json_array_append_new ( + requirements, + json_string (kyc_checks[i]->name))); + return requirements; +} + + /** * Load KYC logic plugin. * @@ -279,20 +323,32 @@ load_logic (const struct GNUNET_CONFIGURATION_Handle *cfg, GNUNET_asprintf (&lib_name, "libtaler_plugin_kyclogic_%s", name); + for (unsigned int i = 0; i<num_kyc_logics; i++) + if (0 == strcmp (lib_name, + kyc_logics[i]->library_name)) + { + GNUNET_free (lib_name); + return kyc_logics[i]; + } plugin = GNUNET_PLUGIN_load (lib_name, (void *) cfg); - if (NULL != plugin) - plugin->library_name = lib_name; - else + if (NULL == plugin) + { GNUNET_free (lib_name); + return NULL; + } + plugin->library_name = lib_name; + plugin->name = GNUNET_strdup (name); + GNUNET_array_append (kyc_logics, + num_kyc_logics, + plugin); return plugin; } /** - * Add check type to global array of checks. - * First checks if the type already exists, otherwise - * adds a new one. + * Add check type to global array of checks. First checks if the type already + * exists, otherwise adds a new one. * * @param check name of the check * @return pointer into the global list @@ -316,9 +372,8 @@ add_check (const char *check) /** - * Parse list of checks from @a checks and build an - * array of aliases into the global checks array - * in @a provided_checks. + * Parse list of checks from @a checks and build an array of aliases into the + * global checks array in @a provided_checks. * * @param[in,out] checks list of checks; clobbered * @param[out] p_checks where to put array of aliases @@ -471,6 +526,14 @@ add_provider (const struct GNUNET_CONFIGURATION_Handle *cfg, } +/** + * Parse configuration @a cfg in section @a section for + * the specification of a KYC trigger. + * + * @param cfg configuration to parse + * @param section configuration section to parse + * @return #GNUNET_OK on success + */ static enum GNUNET_GenericReturnValue add_trigger (const struct GNUNET_CONFIGURATION_Handle *cfg, const char *section) @@ -562,6 +625,29 @@ add_trigger (const struct GNUNET_CONFIGURATION_Handle *cfg, GNUNET_array_append (kyc_triggers, num_kyc_triggers, kt); + for (unsigned int i = 0; i<kt->num_checks; i++) + { + const struct TALER_KYCLOGIC_KycCheck *ck = kt->required_checks[i]; + + if (0 != ck->num_providers) + continue; + if (0 == strcmp (ck->name, + KYC_CHECK_IMPOSSIBLE)) + continue; + { + char *msg; + + GNUNET_asprintf (&msg, + "Required check `%s' cannot be satisfied: not provided by any provider", + ck->name); + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "REQUIRED_CHECKS", + msg); + GNUNET_free (msg); + } + return GNUNET_SYSERR; + } } return GNUNET_OK; } @@ -591,8 +677,8 @@ struct SectionContext * @param section name of the section */ static void -handle_section (void *cls, - const char *section) +handle_provider_section (void *cls, + const char *section) { struct SectionContext *sc = cls; @@ -606,6 +692,21 @@ handle_section (void *cls, sc->result = false; return; } +} + + +/** + * Function to iterate over configuration sections. + * + * @param cls a `struct SectionContext *` + * @param section name of the section + */ +static void +handle_trigger_section (void *cls, + const char *section) +{ + struct SectionContext *sc = cls; + if (0 == strncasecmp (section, "kyc-legitimization-", strlen ("kyc-legitimization-"))) @@ -657,7 +758,10 @@ TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) }; GNUNET_CONFIGURATION_iterate_sections (cfg, - &handle_section, + &handle_provider_section, + &sc); + GNUNET_CONFIGURATION_iterate_sections (cfg, + &handle_trigger_section, &sc); if (! sc.result) { @@ -676,10 +780,11 @@ TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) TALER_KYCLOGIC_kyc_done (); return GNUNET_SYSERR; } - qsort (kyc_triggers, - num_kyc_triggers, - sizeof (struct TALER_KYCLOGIC_KycTrigger *), - &sort_by_timeframe); + if (0 != num_kyc_triggers) + qsort (kyc_triggers, + num_kyc_triggers, + sizeof (struct TALER_KYCLOGIC_KycTrigger *), + &sort_by_timeframe); return GNUNET_OK; } @@ -717,6 +822,7 @@ TALER_KYCLOGIC_kyc_done (void) struct TALER_KYCLOGIC_Plugin *lp = kyc_logics[i]; char *lib_name = lp->library_name; + GNUNET_free (lp->name); GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name, lp)); GNUNET_free (lib_name); @@ -728,6 +834,9 @@ TALER_KYCLOGIC_kyc_done (void) { struct TALER_KYCLOGIC_KycCheck *kc = kyc_checks[i]; + GNUNET_array_grow (kc->providers, + kc->num_providers, + 0); GNUNET_free (kc->name); GNUNET_free (kc); } @@ -797,6 +906,9 @@ eval_trigger (void *cls, struct GNUNET_TIME_Relative duration; bool bump = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check with new amount %s\n", + TALER_amount2s (amount)); duration = GNUNET_TIME_absolute_get_duration (date); if (ttc->have_total) { @@ -812,19 +924,31 @@ eval_trigger (void *cls, else { ttc->total = *amount; + ttc->have_total = true; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check: new total is %s\n", + TALER_amount2s (&ttc->total)); for (unsigned int i = ttc->start; i<num_kyc_triggers; i++) { const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; if (ttc->event != kt->trigger) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u: trigger type does not match\n", + i); continue; + } duration = GNUNET_TIME_relative_max (duration, kt->timeframe); if (GNUNET_TIME_relative_cmp (kt->timeframe, >, duration)) { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u: amount is beyond time limit\n", + i); if (bump) ttc->start = i; return GNUNET_OK; @@ -833,6 +957,9 @@ eval_trigger (void *cls, TALER_amount_cmp (&ttc->total, &kt->threshold)) { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u: amount is below threshold\n", + i); if (bump) ttc->start = i; bump = false; @@ -848,6 +975,9 @@ eval_trigger (void *cls, for (unsigned int k = 0; k<*ttc->needed_cnt; k++) if (ttc->needed[k] == rc) { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC rule #%u already listed\n", + j); found = true; break; } @@ -857,6 +987,11 @@ eval_trigger (void *cls, (*ttc->needed_cnt)++; } } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u (%s) is applicable, %u checks needed so far\n", + i, + ttc->needed[(*ttc->needed_cnt) - 1]->name, + *ttc->needed_cnt); } if (bump) return GNUNET_NO; /* we hit all possible triggers! */ @@ -880,6 +1015,10 @@ struct RemoveContext */ unsigned int *needed_cnt; + /** + * Object with information about collected KYC data. + */ + json_t *kyc_details; }; @@ -903,13 +1042,30 @@ remove_satisfied (void *cls, if (0 != strcasecmp (provider_name, kp->provider_section_name)) continue; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Provider `%s' satisfied\n", + provider_name); for (unsigned int j = 0; j<kp->num_checks; j++) { const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Provider satisfies check `%s'\n", + kc->name); + if (NULL != rc->kyc_details) + { + GNUNET_assert (0 == + json_object_set_new ( + rc->kyc_details, + kc->name, + json_object ())); + } for (unsigned int k = 0; k<*rc->needed_cnt; k++) if (kc == rc->needed[k]) { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Removing check `%s' from list\n", + kc->name); rc->needed[k] = rc->needed[*rc->needed_cnt - 1]; (*rc->needed_cnt)--; if (0 == *rc->needed_cnt) @@ -922,20 +1078,19 @@ remove_satisfied (void *cls, } -const char * +enum GNUNET_DB_QueryStatus TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, const struct TALER_PaytoHashP *h_payto, TALER_KYCLOGIC_KycSatisfiedIterator ki, void *ki_cls, TALER_KYCLOGIC_KycAmountIterator ai, - void *ai_cls) + void *ai_cls, + char **required) { struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; unsigned int needed_cnt = 0; + char *ret; struct GNUNET_TIME_Relative timeframe; - unsigned long long min_cost = ULONG_LONG_MAX; - unsigned int max_checks = 0; - const struct TALER_KYCLOGIC_KycProvider *kp_best = NULL; timeframe = GNUNET_TIME_UNIT_ZERO; for (unsigned int i = 0; i<num_kyc_triggers; i++) @@ -963,7 +1118,40 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, &ttc); } if (0 == needed_cnt) - return NULL; + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + timeframe = GNUNET_TIME_UNIT_ZERO; + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (event != kt->trigger) + continue; + timeframe = GNUNET_TIME_relative_max (timeframe, + kt->timeframe); + } + { + struct GNUNET_TIME_Absolute now; + struct ThresholdTestContext ttc = { + .event = event, + .needed = needed, + .needed_cnt = &needed_cnt + }; + + now = GNUNET_TIME_absolute_get (); + ai (ai_cls, + GNUNET_TIME_absolute_subtract (now, + timeframe), + &eval_trigger, + &ttc); + } + if (0 == needed_cnt) + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } { struct RemoveContext rc = { .needed = needed, @@ -973,14 +1161,206 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, /* Check what provider checks are already satisfied for h_payto (with database), remove those from the 'needed' array. */ - GNUNET_break (0); - // FIXME: do via callback! qs = ki (ki_cls, h_payto, - & - remove_satisfied, + &remove_satisfied, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + } + if (0 == needed_cnt) + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + { + struct RemoveContext rc = { + .needed = needed, + .needed_cnt = &needed_cnt + }; + enum GNUNET_DB_QueryStatus qs; + + /* Check what provider checks are already satisfied for h_payto (with + database), remove those from the 'needed' array. */ + qs = ki (ki_cls, + h_payto, + &remove_satisfied, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + } + if (0 == needed_cnt) + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + ret = NULL; + for (unsigned int k = 0; k<needed_cnt; k++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = needed[k]; + + if (NULL == ret) + { + ret = GNUNET_strdup (kc->name); + } + else /* append */ + { + char *tmp = ret; + + GNUNET_asprintf (&ret, + "%s %s", + tmp, + kc->name); + GNUNET_free (tmp); + } + } + *required = ret; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +void +TALER_KYCLOGIC_kyc_get_details ( + const char *logic_name, + TALER_KYCLOGIC_DetailsCallback cb, + void *cb_cls) +{ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != + strcmp (kp->logic->name, + logic_name)) + continue; + if (GNUNET_OK != + cb (cb_cls, + kp->pd, + kp->logic->cls)) + return; + } +} + + +enum GNUNET_DB_QueryStatus +TALER_KYCLOGIC_check_satisfied (char **requirements, + const struct TALER_PaytoHashP *h_payto, + json_t **kyc_details, + TALER_KYCLOGIC_KycSatisfiedIterator ki, + void *ki_cls, + bool *satisfied) +{ + struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; + unsigned int needed_cnt = 0; + + if (NULL == requirements) + { + *satisfied = true; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + { + char *req = *requirements; + + for (const char *tok = strtok (req, " "); + NULL != tok; + tok = strtok (NULL, " ")) + needed[needed_cnt++] = add_check (tok); + GNUNET_free (req); + *requirements = NULL; + } + + { + struct RemoveContext rc = { + .needed = needed, + .needed_cnt = &needed_cnt, + }; + enum GNUNET_DB_QueryStatus qs; + + rc.kyc_details = json_object (); + GNUNET_assert (NULL != rc.kyc_details); + + /* Check what provider checks are already satisfied for h_payto (with + database), remove those from the 'needed' array. */ + qs = ki (ki_cls, + h_payto, + &remove_satisfied, &rc); - GNUNET_break (qs >= 0); // FIXME: handle DB failure more nicely? + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + *satisfied = false; + return qs; + } + if (0 != needed_cnt) + { + json_decref (rc.kyc_details); + *kyc_details = NULL; + } + else + { + *kyc_details = rc.kyc_details; + } + } + *satisfied = (0 == needed_cnt); + + { + char *res = NULL; + + for (unsigned int i = 0; i<needed_cnt; i++) + { + const struct TALER_KYCLOGIC_KycCheck *need = needed[i]; + + if (NULL == res) + { + res = GNUNET_strdup (need->name); + } + else + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s %s", + res, + need->name); + GNUNET_free (res); + res = tmp; + } + } + *requirements = res; + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_requirements_to_logic (const char *requirements, + enum TALER_KYCLOGIC_KycUserType ut, + struct TALER_KYCLOGIC_Plugin **plugin, + struct TALER_KYCLOGIC_ProviderDetails **pd, + const char **configuration_section) +{ + struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; + unsigned int needed_cnt = 0; + unsigned long long min_cost = ULLONG_MAX; + unsigned int max_checks = 0; + const struct TALER_KYCLOGIC_KycProvider *kp_best = NULL; + + if (NULL == requirements) + return GNUNET_NO; + { + char *req = GNUNET_strdup (requirements); + + for (const char *tok = strtok (req, " "); + NULL != tok; + tok = strtok (NULL, " ")) + needed[needed_cnt++] = add_check (tok); + GNUNET_free (req); } /* Count maximum number of remaining checks covered by any @@ -990,6 +1370,8 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; unsigned int matched = 0; + if (kp->user_type != ut) + continue; for (unsigned int j = 0; j<kp->num_checks; j++) { const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; @@ -1004,6 +1386,8 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, max_checks = GNUNET_MAX (max_checks, matched); } + if (0 == max_checks) + return GNUNET_SYSERR; /* Find min-cost provider covering max_checks. */ for (unsigned int i = 0; i<num_kyc_providers; i++) @@ -1011,6 +1395,8 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; unsigned int matched = 0; + if (kp->user_type != ut) + continue; for (unsigned int j = 0; j<kp->num_checks; j++) { const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; @@ -1029,34 +1415,98 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, kp_best = kp; } } - GNUNET_assert (NULL != kp_best); - return kp_best->provider_section_name; + *plugin = kp_best->logic; + *pd = kp_best->pd; + *configuration_section = kp_best->provider_section_name; + return GNUNET_OK; } enum GNUNET_GenericReturnValue -TALER_KYCLOGIC_kyc_get_logic (const char *provider_section_name, - struct TALER_KYCLOGIC_Plugin **plugin, - struct TALER_KYCLOGIC_ProviderDetails **pd) +TALER_KYCLOGIC_lookup_logic (const char *name, + struct TALER_KYCLOGIC_Plugin **plugin, + struct TALER_KYCLOGIC_ProviderDetails **pd, + const char **provider_section) { for (unsigned int i = 0; i<num_kyc_providers; i++) { struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; if (0 != - strcasecmp (provider_section_name, + strcasecmp (name, kp->provider_section_name)) continue; *plugin = kp->logic; *pd = kp->pd; + *provider_section = kp->provider_section_name; + return GNUNET_OK; + } + for (unsigned int i = 0; i<num_kyc_logics; i++) + { + struct TALER_KYCLOGIC_Plugin *logic = kyc_logics[i]; + + if (0 != + strcasecmp (logic->name, + name)) + continue; + *plugin = logic; + *pd = NULL; + *provider_section = NULL; return GNUNET_OK; } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Provider `%s' unknown\n", - provider_section_name); + name); return GNUNET_SYSERR; } +void +TALER_KYCLOGIC_kyc_iterate_thresholds ( + enum TALER_KYCLOGIC_KycTriggerEvent event, + TALER_KYCLOGIC_KycThresholdIterator it, + void *it_cls) +{ + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (event != kt->trigger) + continue; + it (it_cls, + &kt->threshold); + } +} + + +void +TALER_KYCLOGIC_lookup_checks (const char *section_name, + unsigned int *num_checks, + char ***provided_checks) +{ + *num_checks = 0; + *provided_checks = NULL; + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != + strcasecmp (section_name, + kp->provider_section_name)) + continue; + *num_checks = kp->num_checks; + if (0 != kp->num_checks) + { + char **pc = GNUNET_new_array (kp->num_checks, + char *); + for (unsigned int i = 0; i<kp->num_checks; i++) + pc[i] = GNUNET_strdup (kp->provided_checks[i]->name); + *provided_checks = pc; + } + return; + } +} + + /* end of taler-exchange-httpd_kyc.c */ |