Commit 089b70b0 authored by Vladimír Čunát's avatar Vladimír Čunát

struct kr_zonecut::nsset: migrate from map_t to trie_t

parent d9317141
......@@ -594,9 +594,8 @@ static int init_resolver(struct engine *engine)
knot_edns_init(engine->resolver.opt_rr, KR_EDNS_PAYLOAD, 0, KR_EDNS_VERSION, engine->pool);
/* Use default TLS padding */
engine->resolver.tls_padding = -1;
/* Set default root hints */
/* Empty init; filled via ./lua/config.lua */
kr_zonecut_init(&engine->resolver.root_hints, (const uint8_t *)"", engine->pool);
kr_zonecut_set_sbelt(&engine->resolver, &engine->resolver.root_hints);
/* Open NS rtt + reputation cache */
lru_create(&engine->resolver.cache_rtt, LRU_RTT_SIZE, engine->pool, NULL);
lru_create(&engine->resolver.cache_rep, LRU_REP_SIZE, engine->pool, NULL);
......
......@@ -19,7 +19,8 @@ if not cache.current_size then
cache.size = 100 * MB
end
if kres.context().root_hints.nsset.root == nil then
-- If no addresses for root servers are set, load them from the default file
if require('ffi').C.kr_zonecut_is_empty(kres.context().root_hints) then
_hint_root_file()
end
......
......@@ -119,12 +119,13 @@ typedef struct {
size_t len;
size_t cap;
} ranked_rr_array_t;
typedef struct trie trie_t;
struct kr_zonecut {
knot_dname_t *name;
knot_rrset_t *key;
knot_rrset_t *trust_anchor;
struct kr_zonecut *parent;
map_t nsset;
trie_t *nsset;
knot_mm_t *pool;
};
typedef struct {
......@@ -286,6 +287,7 @@ int kr_ranked_rrarray_add(ranked_rr_array_t *, const knot_rrset_t *, uint8_t, _B
void kr_qflags_set(struct kr_qflags *, struct kr_qflags);
void kr_qflags_clear(struct kr_qflags *, struct kr_qflags);
int kr_zonecut_add(struct kr_zonecut *, const knot_dname_t *, const knot_rdata_t *);
_Bool kr_zonecut_is_empty(struct kr_zonecut *);
void kr_zonecut_set(struct kr_zonecut *, const knot_dname_t *);
uint64_t kr_now();
knot_rrset_t *kr_ta_get(map_t *, const knot_dname_t *);
......
......@@ -57,6 +57,7 @@ typedef void (*trace_callback_f)(struct kr_request *);
struct ranked_rr_array_entry
ranked_rr_array_entry_t
ranked_rr_array_t
trie_t
struct kr_zonecut
kr_qarray_t
struct kr_rplan
......@@ -154,6 +155,7 @@ EOF
kr_qflags_set
kr_qflags_clear
kr_zonecut_add
kr_zonecut_is_empty
kr_zonecut_set
kr_now
# Trust anchors
......
......@@ -192,23 +192,30 @@ static inline int pack_obj_del(pack_t *pack, const uint8_t *obj, pack_objlen_t l
return -1;
}
/** Clone a pack into a mempool (can be NULL).
* @return NULL on allocation failure. */
static inline pack_t * pack_clone(const pack_t *src, knot_mm_t *pool)
/** Clone a pack, replacing destination pack; (*dst == NULL) is valid input.
* @return kr_error(ENOMEM) on allocation failure. */
static inline int pack_clone(pack_t **dst, const pack_t *src, knot_mm_t *pool)
{
pack_t *dst = mm_alloc(pool, sizeof(pack_t));
if (!dst) return dst;
pack_init(*dst);
/* Clone data only if needed */
if (!pack_head(*src)) return dst;
int ret = array_reserve_mm(*dst, src->len, kr_memreserve, pool);
if (!dst || !src) {
assert(false);
return kr_error(EINVAL);
}
/* Get a valid pack_t. */
if (!*dst) {
*dst = mm_alloc(pool, sizeof(pack_t));
if (!*dst) return kr_error(ENOMEM);
pack_init(**dst);
/* Clone data only if needed */
if (src->len == 0) return kr_ok();
}
/* Replace the contents of the pack_t. */
int ret = array_reserve_mm(**dst, src->len, kr_memreserve, pool);
if (ret < 0) {
mm_free(pool, dst);
return NULL;
return kr_error(ENOMEM);
}
memcpy(dst->at, src->at, src->len);
dst->len = src->len;
return dst;
memcpy((*dst)->at, src->at, src->len);
(*dst)->len = src->len;
return kr_ok();
}
#ifdef __cplusplus
......
......@@ -82,7 +82,7 @@ static void update_nsrep_set(struct kr_nsrep *ns, const knot_dname_t *name, uint
#undef ADDR_SET
static unsigned eval_addr_set(pack_t *addr_set, struct kr_context *ctx,
static unsigned eval_addr_set(const pack_t *addr_set, struct kr_context *ctx,
unsigned score, uint8_t *addr[])
{
kr_nsrep_rtt_lru_t *rtt_cache = ctx->cache_rtt;
......@@ -209,9 +209,8 @@ get_next_iterator :
return rtt_cache_entry_score[0];
}
static int eval_nsrep(const char *k, void *v, void *baton)
static int eval_nsrep(const knot_dname_t *owner, const pack_t *addr_set, struct kr_query *qry)
{
struct kr_query *qry = baton;
struct kr_nsrep *ns = &qry->ns;
struct kr_context *ctx = ns->ctx;
unsigned score = KR_NS_MAX_SCORE;
......@@ -220,8 +219,8 @@ static int eval_nsrep(const char *k, void *v, void *baton)
/* Fetch NS reputation */
if (ctx->cache_rep) {
unsigned *cached = lru_get_try(ctx->cache_rep, k,
knot_dname_size((const uint8_t *)k));
unsigned *cached = lru_get_try(ctx->cache_rep, (const char *)owner,
knot_dname_size(owner));
if (cached) {
reputation = *cached;
}
......@@ -229,7 +228,6 @@ static int eval_nsrep(const char *k, void *v, void *baton)
/* Favour nameservers with unknown addresses to probe them,
* otherwise discover the current best address for the NS. */
pack_t *addr_set = (pack_t *)v;
if (addr_set->len == 0) {
score = KR_NS_UNKNOWN;
/* If the server doesn't have IPv6, give it disadvantage. */
......@@ -261,19 +259,19 @@ static int eval_nsrep(const char *k, void *v, void *baton)
return kr_ok();
} else if (score <= ns->score &&
(score < KR_NS_LONG || qry->flags.NO_THROTTLE)) {
update_nsrep_set(ns, (const knot_dname_t *)k, addr_choice, score);
update_nsrep_set(ns, owner, addr_choice, score);
ns->reputation = reputation;
} else if ((kr_rand_uint(100) < 10) &&
(kr_rand_uint(KR_NS_MAX_SCORE) >= score)) {
/* With 10% chance probe server with a probability
* given by its RTT / MAX_RTT. */
update_nsrep_set(ns, (const knot_dname_t *)k, addr_choice, score);
update_nsrep_set(ns, owner, addr_choice, score);
ns->reputation = reputation;
return 1; /* Stop evaluation */
} else if (ns->score > KR_NS_MAX_SCORE) {
/* Check if any server was already selected.
* If no, pick current server and continue evaluation. */
update_nsrep_set(ns, (const knot_dname_t *)k, addr_choice, score);
update_nsrep_set(ns, owner, addr_choice, score);
ns->reputation = reputation;
}
......@@ -339,7 +337,18 @@ int kr_nsrep_elect(struct kr_query *qry, struct kr_context *ctx)
struct kr_nsrep *ns = &qry->ns;
ELECT_INIT(ns, ctx);
int ret = map_walk(&qry->zone_cut.nsset, eval_nsrep, qry);
int ret = kr_ok();
trie_it_t *it;
for (it = trie_it_begin(qry->zone_cut.nsset); !trie_it_finished(it);
trie_it_next(it)) {
ret = eval_nsrep(/* we trust it's a correct dname */
(const knot_dname_t *)trie_it_key(it, NULL),
(const pack_t *)*trie_it_val(it), qry);
if (ret) break;
}
trie_it_free(it);
if (qry->ns.score <= KR_NS_MAX_SCORE && qry->ns.score >= KR_NS_LONG) {
/* This is a low-reliability probe,
* go with TCP to get ICMP reachability check. */
......@@ -357,7 +366,7 @@ int kr_nsrep_elect_addr(struct kr_query *qry, struct kr_context *ctx)
/* Get address list for this NS */
struct kr_nsrep *ns = &qry->ns;
ELECT_INIT(ns, ctx);
pack_t *addr_set = map_get(&qry->zone_cut.nsset, (const char *)ns->name);
pack_t *addr_set = kr_zonecut_find(&qry->zone_cut, ns->name);
if (!addr_set) {
return kr_error(ENOENT);
}
......
......@@ -268,7 +268,7 @@ static int ns_fetch_cut(struct kr_query *qry, const knot_dname_t *requested_name
}
/* Check if any DNSKEY found for cached cut */
if (qry->flags.DNSSEC_WANT && cut_found.key == NULL &&
!kr_zonecut_has_glue(&cut_found)) {
kr_zonecut_is_empty(&cut_found)) {
/* Cut found and there are no proofs of zone insecurity.
* But no DNSKEY found and no glue fetched.
* We have got circular dependency - must fetch A\AAAA
......@@ -1428,7 +1428,7 @@ int kr_resolve_produce(struct kr_request *request, struct sockaddr **dst, int *t
}
kr_nsrep_elect(qry, request->ctx);
if (qry->ns.score > KR_NS_MAX_SCORE) {
if (!qry->zone_cut.nsset.root) {
if (kr_zonecut_is_empty(&qry->zone_cut)) {
VERBOSE_MSG(qry, "=> no NS with an address\n");
} else {
VERBOSE_MSG(qry, "=> no valid NS left\n");
......
......@@ -60,15 +60,25 @@ int kr_zonecut_init(struct kr_zonecut *cut, const knot_dname_t *name, knot_mm_t
cut->key = NULL;
cut->trust_anchor = NULL;
cut->parent = NULL;
cut->nsset = map_make(pool);
return kr_ok();
cut->nsset = trie_create(pool);
return cut->name && cut->nsset ? kr_ok() : kr_error(ENOMEM);
}
static int free_addr_set(const char *k, void *v, void *baton)
/** Completely free a pack_t. */
static inline void free_addr_set(pack_t *pack, knot_mm_t *pool)
{
if (unlikely(!pack)) {
/* promised we don't store NULL packs */
assert(false);
return;
}
pack_clear_mm(*pack, mm_free, pool);
mm_free(pool, pack);
}
/** Trivial wrapper for use in trie_apply, due to ugly casting. */
static int free_addr_set_cb(trie_val_t *v, void *pool)
{
pack_t *pack = v;
pack_clear_mm(*pack, mm_free, baton);
mm_free(baton, pack);
free_addr_set(*v, pool);
return kr_ok();
}
......@@ -78,8 +88,11 @@ void kr_zonecut_deinit(struct kr_zonecut *cut)
return;
}
mm_free(cut->pool, cut->name);
map_walk(&cut->nsset, free_addr_set, cut->pool);
map_clear(&cut->nsset);
if (cut->nsset) {
trie_apply(cut->nsset, free_addr_set_cb, cut->pool);
trie_free(cut->nsset);
cut->nsset = NULL;
}
knot_rrset_free(&cut->key, cut->pool);
knot_rrset_free(&cut->trust_anchor, cut->pool);
cut->name = NULL;
......@@ -99,43 +112,31 @@ void kr_zonecut_set(struct kr_zonecut *cut, const knot_dname_t *name)
cut->trust_anchor = ta;
}
static int copy_addr_set(const char *k, void *v, void *baton)
{
pack_t *addr_set = v;
struct kr_zonecut *dst = baton;
/* Clone addr_set pack */
pack_t *new_set = mm_alloc(dst->pool, sizeof(*new_set));
if (!new_set) {
return kr_error(ENOMEM);
}
pack_init(*new_set);
/* Clone data only if needed */
if (addr_set->len > 0) {
new_set->at = mm_alloc(dst->pool, addr_set->len);
if (!new_set->at) {
mm_free(dst->pool, new_set);
return kr_error(ENOMEM);
}
memcpy(new_set->at, addr_set->at, addr_set->len);
new_set->len = addr_set->len;
new_set->cap = addr_set->len;
}
/* Reinsert */
if (map_set(&dst->nsset, k, new_set) != 0) {
pack_clear_mm(*new_set, mm_free, dst->pool);
mm_free(dst->pool, new_set);
return kr_error(ENOMEM);
}
return kr_ok();
}
int kr_zonecut_copy(struct kr_zonecut *dst, const struct kr_zonecut *src)
{
if (!dst || !src) {
return kr_error(EINVAL);
}
/* We're not touching src nsset, I promise */
return map_walk((map_t *)&src->nsset, copy_addr_set, dst);
if (!dst->nsset) {
dst->nsset = trie_create(dst->pool);
}
/* Copy the contents, one by one. */
int ret = kr_ok();
trie_it_t *it;
for (it = trie_it_begin(src->nsset); !trie_it_finished(it); trie_it_next(it)) {
size_t klen;
const char * const k = trie_it_key(it, &klen);
pack_t **new_pack = (pack_t **)trie_get_ins(dst->nsset, k, klen);
if (!new_pack) {
ret = kr_error(ENOMEM);
break;
}
const pack_t *old_pack = *trie_it_val(it);
ret = pack_clone(new_pack, old_pack, dst->pool);
if (ret) break;
}
trie_it_free(it);
return ret;
}
int kr_zonecut_copy_trust(struct kr_zonecut *dst, const struct kr_zonecut *src)
......@@ -168,18 +169,16 @@ int kr_zonecut_copy_trust(struct kr_zonecut *dst, const struct kr_zonecut *src)
int kr_zonecut_add(struct kr_zonecut *cut, const knot_dname_t *ns, const knot_rdata_t *rdata)
{
if (!cut || !ns) {
if (!cut || !ns || !cut->nsset) {
return kr_error(EINVAL);
}
/* Fetch/insert nameserver. */
pack_t *pack = kr_zonecut_find(cut, ns);
if (pack == NULL) {
pack = mm_alloc(cut->pool, sizeof(*pack));
if (!pack || (map_set(&cut->nsset, (const char *)ns, pack) != 0)) {
mm_free(cut->pool, pack);
return kr_error(ENOMEM);
}
pack_init(*pack);
/* Get a pack_t for the ns. */
pack_t **pack = (pack_t **)trie_get_ins(cut->nsset, (const char *)ns, knot_dname_size(ns));
if (!pack) return kr_error(ENOMEM);
if (*pack == NULL) {
*pack = mm_alloc(cut->pool, sizeof(pack_t));
if (*pack == NULL) return kr_error(ENOMEM);
pack_init(**pack);
}
/* Insert data (if has any) */
if (rdata == NULL) {
......@@ -188,15 +187,15 @@ int kr_zonecut_add(struct kr_zonecut *cut, const knot_dname_t *ns, const knot_rd
/* Check for duplicates */
uint16_t rdlen = knot_rdata_rdlen(rdata);
uint8_t *raw_addr = knot_rdata_data(rdata);
if (pack_obj_find(pack, raw_addr, rdlen)) {
if (pack_obj_find(*pack, raw_addr, rdlen)) {
return kr_ok();
}
/* Push new address */
int ret = pack_reserve_mm(*pack, 1, rdlen, kr_memreserve, cut->pool);
int ret = pack_reserve_mm(**pack, 1, rdlen, kr_memreserve, cut->pool);
if (ret != 0) {
return kr_error(ENOMEM);
}
return pack_obj_push(pack, raw_addr, rdlen);
return pack_obj_push(*pack, raw_addr, rdlen);
}
int kr_zonecut_del(struct kr_zonecut *cut, const knot_dname_t *ns, const knot_rdata_t *rdata)
......@@ -217,8 +216,10 @@ int kr_zonecut_del(struct kr_zonecut *cut, const knot_dname_t *ns, const knot_rd
}
/* No servers left, remove NS from the set. */
if (pack->len == 0) {
free_addr_set((const char *)ns, pack, cut->pool);
return map_del(&cut->nsset, (const char *)ns);
free_addr_set(pack, cut->pool);
ret = trie_del(cut->nsset, (const char *)ns, knot_dname_size(ns), NULL);
assert(ret == 0); /* only KNOT_ENOENT and that *can't* happen */
return (ret == 0) ? kr_ok() : kr_error(ret);
}
return ret;
......@@ -231,12 +232,15 @@ int kr_zonecut_del_all(struct kr_zonecut *cut, const knot_dname_t *ns)
}
/* Find the address list; then free and remove it. */
pack_t *pack = kr_zonecut_find(cut, ns);
if (pack == NULL) {
pack_t *pack;
int ret = trie_del(cut->nsset, (const char *)ns, knot_dname_size(ns),
(trie_val_t *)&pack);
if (ret) { /* deletion failed */
assert(ret == KNOT_ENOENT);
return kr_error(ENOENT);
}
free_addr_set((const char *)ns, pack, cut->pool);
return map_del(&cut->nsset, (const char *)ns);
free_addr_set(pack, cut->pool);
return kr_ok();
}
pack_t *kr_zonecut_find(struct kr_zonecut *cut, const knot_dname_t *ns)
......@@ -244,58 +248,39 @@ pack_t *kr_zonecut_find(struct kr_zonecut *cut, const knot_dname_t *ns)
if (!cut || !ns) {
return NULL;
}
const char *key = (const char *)ns;
map_t *nsset = &cut->nsset;
return map_get(nsset, key);
trie_val_t *val = trie_get_try(cut->nsset, (const char *)ns, knot_dname_size(ns));
/* we get pointer to the pack_t pointer */
return val ? (pack_t *)*val : NULL;
}
static int has_glue(const char *k, void *v, void *baton)
static int has_address(trie_val_t *v, void *baton_)
{
bool *glue_found = (bool *)baton;
if (*glue_found) {
assert(false);
return 1; /* short-circuit */
}
pack_t *pack = (pack_t *)v;
if (pack != NULL && pack->len != 0) {
*glue_found = true;
return 1; /* short-circuit */
}
return kr_ok();
const pack_t *pack = *v;
const bool found = pack != NULL && pack->len != 0;
return found;
}
bool kr_zonecut_has_glue(struct kr_zonecut *cut)
bool kr_zonecut_is_empty(struct kr_zonecut *cut)
{
if (!cut) {
return false;
if (!cut || !cut->nsset) {
assert(false);
return true;
}
bool glue_found = false;
map_t *nsset = &cut->nsset;
map_walk(nsset, has_glue, &glue_found);
return glue_found;
return !trie_apply(cut->nsset, has_address, NULL);
}
int kr_zonecut_set_sbelt(struct kr_context *ctx, struct kr_zonecut *cut)
{
if (!ctx || !cut) {
if (!ctx || !cut || !ctx->root_hints.nsset) {
return kr_error(EINVAL);
}
update_cut_name(cut, U8(""));
map_walk(&cut->nsset, free_addr_set, cut->pool);
map_clear(&cut->nsset);
trie_apply(cut->nsset, free_addr_set_cb, cut->pool);
trie_clear(cut->nsset);
update_cut_name(cut, U8(""));
/* Copy root hints from resolution context. */
int ret = 0;
if (ctx->root_hints.nsset.root) {
ret = kr_zonecut_copy(cut, &ctx->root_hints);
}
return ret;
return kr_zonecut_copy(cut, &ctx->root_hints);
}
/** Fetch address for zone cut. Any rank is accepted (i.e. glue as well). */
......
......@@ -16,10 +16,10 @@
#pragma once
#include "lib/generic/map.h"
#include "lib/generic/pack.h"
#include "lib/defines.h"
#include "lib/cache/api.h"
#include "lib/defines.h"
#include "lib/generic/pack.h"
#include "lib/generic/trie.h"
struct kr_rplan;
struct kr_context;
......@@ -32,7 +32,7 @@ struct kr_zonecut {
knot_rrset_t* key; /**< Zone cut DNSKEY. */
knot_rrset_t* trust_anchor; /**< Current trust anchor. */
struct kr_zonecut *parent; /**< Parent zone cut. */
map_t nsset; /**< Map of nameserver => address_set. */
trie_t *nsset; /**< Map of nameserver => address_set (pack_t). */
knot_mm_t *pool; /**< Memory pool. */
};
......@@ -62,11 +62,13 @@ void kr_zonecut_deinit(struct kr_zonecut *cut);
KR_EXPORT
void kr_zonecut_set(struct kr_zonecut *cut, const knot_dname_t *name);
/**
/**
* Copy zone cut, including all data. Does not copy keys and trust anchor.
* @param dst destination zone cut
* @param src source zone cut
* @return 0 or an error code
* @return 0 or an error code; If it fails with kr_error(ENOMEM),
* it may be in a half-filled state, but it's safe to deinit...
* @note addresses for names in `src` get replaced and others are left as they were.
*/
KR_EXPORT
int kr_zonecut_copy(struct kr_zonecut *dst, const struct kr_zonecut *src);
......@@ -151,9 +153,11 @@ int kr_zonecut_find_cached(struct kr_context *ctx, struct kr_zonecut *cut,
const knot_dname_t *name, const struct kr_query *qry,
bool * restrict secured);
/**
* Check if any glue addresses are present in zone cut
* Check if any address is present in the zone cut.
*
* @param cut zone cut to check
* @return true/false
*/
bool kr_zonecut_has_glue(struct kr_zonecut *cut);
KR_EXPORT
bool kr_zonecut_is_empty(struct kr_zonecut *cut);
......@@ -506,27 +506,24 @@ static char* hint_get(void *env, struct kr_module *module, const char *args)
return result;
}
/** Retrieve hint list. */
static int pack_hint(const char *k, void *v, void *baton)
{
char nsname_str[KNOT_DNAME_MAXLEN] = {'\0'};
knot_dname_to_str(nsname_str, (const uint8_t *)k, sizeof(nsname_str));
JsonNode *root_node = baton;
JsonNode *addr_list = pack_addrs((pack_t *)v);
if (!addr_list) {
return kr_error(ENOMEM);
}
json_append_member(root_node, nsname_str, addr_list);
return kr_ok();
}
/** @internal Pack all hints into serialized JSON. */
static char* pack_hints(struct kr_zonecut *hints) {
char *result = NULL;
JsonNode *root_node = json_mkobject();
if (map_walk(&hints->nsset, pack_hint, root_node) == 0) {
result = json_encode(root_node);
}
trie_it_t *it;
for (it = trie_it_begin(hints->nsset); !trie_it_finished(it); trie_it_next(it)) {
char nsname_str[KNOT_DNAME_MAXLEN] = {'\0'};
knot_dname_to_str(nsname_str,
/* here we trust that it's a correct dname */
(const knot_dname_t *)trie_it_key(it, NULL),
sizeof(nsname_str));
JsonNode *addr_list = pack_addrs((pack_t *)*trie_it_val(it));
if (!addr_list) goto error;
json_append_member(root_node, nsname_str, addr_list);
}
result = json_encode(root_node);
error:
trie_it_free(it);
json_delete(root_node);
return result;
}
......
......@@ -37,19 +37,22 @@ static void test_zonecut_params(void **state)
static void test_zonecut_copy(void **state)
{
const knot_dname_t *root = (const uint8_t *)"";
const knot_dname_t *n_root = (const uint8_t *)"";
struct kr_zonecut cut1, cut2;
kr_zonecut_init(&cut1, root, NULL);
kr_zonecut_init(&cut2, root, NULL);
kr_zonecut_init(&cut1, n_root, NULL);
kr_zonecut_init(&cut2, n_root, NULL);
/* Insert some values */
assert_int_equal(kr_zonecut_add(&cut1, (const uint8_t *)"dead", NULL), 0);
assert_int_equal(kr_zonecut_add(&cut1, (const uint8_t *)"beef", NULL), 0);
const knot_dname_t
*n_1 = (const uint8_t *)"\4dead",
*n_2 = (const uint8_t *)"\3bee\1f";
assert_int_equal(kr_zonecut_add(&cut1, n_1, NULL), 0);
assert_int_equal(kr_zonecut_add(&cut1, n_2, NULL), 0);
/* Copy */
assert_int_equal(kr_zonecut_copy(&cut2, &cut1), 0);
/* Check if exist */
assert_non_null(kr_zonecut_find(&cut2, (const uint8_t *)"dead"));
assert_non_null(kr_zonecut_find(&cut2, (const uint8_t *)"beef"));
assert_null(kr_zonecut_find(&cut2, (const uint8_t *)"corn"));
assert_non_null(kr_zonecut_find(&cut2, n_1));
assert_non_null(kr_zonecut_find(&cut2, n_2));
assert_null(kr_zonecut_find(&cut2, (const uint8_t *)"\5death"));
kr_zonecut_deinit(&cut1);
kr_zonecut_deinit(&cut2);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment