blob: 2ea509b25c3a73d411d187955128b018316e3f76 [file] [log] [blame]
Radek Krejci3e6632f2021-03-22 22:08:21 +01001/**
2 * @file plugins.c
3 * @author Radek Krejci <rkrejci@cesnet.cz>
4 * @brief Manipulate with the type and extension plugins.
5 *
6 * Copyright (c) 2021 CESNET, z.s.p.o.
7 *
8 * This source code is licensed under BSD 3-Clause License (the "License").
9 * You may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * https://opensource.org/licenses/BSD-3-Clause
13 */
14
Radek Krejci968d7552021-03-26 20:33:51 +010015#define _GNU_SOURCE
16
Radek Krejci3e6632f2021-03-22 22:08:21 +010017#include "plugins.h"
18#include "plugins_internal.h"
19
20#include <assert.h>
Radek Krejci968d7552021-03-26 20:33:51 +010021#include <dirent.h>
Radek Krejci3e6632f2021-03-22 22:08:21 +010022#include <dlfcn.h>
Radek Krejci968d7552021-03-26 20:33:51 +010023#include <errno.h>
24#include <limits.h>
Radek Krejci3e6632f2021-03-22 22:08:21 +010025#include <pthread.h>
Radek Krejcibf940f92021-03-24 21:04:13 +010026#include <stddef.h>
Radek Krejci0aa1f702021-04-01 16:16:19 +020027#include <stdint.h>
28#include <stdio.h>
29#include <stdlib.h>
Radek Krejci3e6632f2021-03-22 22:08:21 +010030#include <string.h>
31
32#include "common.h"
Radek Krejci12a28c72021-04-06 17:23:37 +020033#include "config.h"
Radek Krejci3e6632f2021-03-22 22:08:21 +010034#include "plugins_exts.h"
35#include "plugins_types.h"
Radek Krejci0aa1f702021-04-01 16:16:19 +020036#include "set.h"
Radek Krejci3e6632f2021-03-22 22:08:21 +010037
38/*
39 * internal type plugins records
40 */
41extern const struct lyplg_type_record plugins_binary[];
42extern const struct lyplg_type_record plugins_bits[];
43extern const struct lyplg_type_record plugins_boolean[];
44extern const struct lyplg_type_record plugins_decimal64[];
45extern const struct lyplg_type_record plugins_empty[];
46extern const struct lyplg_type_record plugins_enumeration[];
47extern const struct lyplg_type_record plugins_identityref[];
48extern const struct lyplg_type_record plugins_instanceid[];
49extern const struct lyplg_type_record plugins_integer[];
50extern const struct lyplg_type_record plugins_leafref[];
51extern const struct lyplg_type_record plugins_string[];
52extern const struct lyplg_type_record plugins_union[];
53
Michal Vaskode4a3412021-04-14 15:38:27 +020054/*
55 * ietf-inet-types
56 */
Michal Vasko3159e782021-05-03 15:12:35 +020057extern const struct lyplg_type_record plugins_ipv4_address[];
Michal Vasko82bf15e2021-05-06 16:01:56 +020058extern const struct lyplg_type_record plugins_ipv4_address_no_zone[];
Michal Vasko7caa3e62021-05-03 14:59:25 +020059extern const struct lyplg_type_record plugins_ipv6_address[];
Michal Vasko18a4a732021-05-06 16:21:44 +020060extern const struct lyplg_type_record plugins_ipv6_address_no_zone[];
Michal Vasko15dc9fa2021-05-03 14:33:05 +020061extern const struct lyplg_type_record plugins_ipv4_prefix[];
62extern const struct lyplg_type_record plugins_ipv6_prefix[];
Michal Vasko3e52de52021-04-13 13:45:55 +020063
Radek Krejci3e6632f2021-03-22 22:08:21 +010064/*
Michal Vaskode4a3412021-04-14 15:38:27 +020065 * ietf-yang-types
66 */
67extern const struct lyplg_type_record plugins_date_and_time[];
Michal Vaskode4a3412021-04-14 15:38:27 +020068extern const struct lyplg_type_record plugins_xpath10[];
69
70/*
Michal Vasko7a53c7d2021-08-06 11:28:57 +020071 * ietf-netconf-acm
72 */
73extern const struct lyplg_type_record plugins_node_instanceid[];
74
75/*
Radek Krejci3e6632f2021-03-22 22:08:21 +010076 * internal extension plugins records
77 */
78extern struct lyplg_ext_record plugins_metadata[];
79extern struct lyplg_ext_record plugins_nacm[];
80extern struct lyplg_ext_record plugins_yangdata[];
81
82static pthread_mutex_t plugins_guard = PTHREAD_MUTEX_INITIALIZER;
83
84/**
85 * @brief Counter for currently present contexts able to refer to the loaded plugins.
86 *
87 * Plugins are shared among all the created contexts. They are loaded with the creation of the very first context and
88 * unloaded with the destroy of the last context. Therefore, to reload the list of plugins, all the contexts must be
89 * destroyed and with the creation of a first new context after that, the plugins will be reloaded.
90 */
91static uint32_t context_refcount = 0;
92
93/**
94 * @brief Record describing an implemented extension.
95 *
96 * Matches ::lyplg_ext_record and ::lyplg_type_record
97 */
98struct lyplg_record {
99 const char *module; /**< name of the module where the extension/type is defined */
100 const char *revision; /**< optional module revision - if not specified, the plugin applies to any revision,
101 which is not an optimal approach due to a possible future revisions of the module.
102 Instead, there should be defined multiple items in the plugins list, each with the
103 different revision, but all with the same pointer to the plugin functions. The
104 only valid use case for the NULL revision is the case the module has no revision. */
105 const char *name; /**< name of the extension/typedef */
106 int8_t plugin[]; /**< specific plugin type's data - ::lyplg_ext or ::lyplg_type */
107};
108
Michal Vasko9e2bc702021-06-09 11:43:36 +0200109#ifndef STATIC
Radek Krejcibf940f92021-03-24 21:04:13 +0100110static struct ly_set plugins_handlers = {0};
Michal Vasko9e2bc702021-06-09 11:43:36 +0200111#endif
Radek Krejci3e6632f2021-03-22 22:08:21 +0100112static struct ly_set plugins_types = {0};
113static struct ly_set plugins_extensions = {0};
114
115/**
116 * @brief Iterate over list of loaded plugins of the given @p type.
117 *
118 * @param[in] type Type of the plugins to iterate.
119 * @param[in,out] index The iterator - set to 0 for the first call.
120 * @return The plugin records, NULL if no more record is available.
121 */
122static struct lyplg_record *
123plugins_iter(enum LYPLG type, uint32_t *index)
124{
125 struct ly_set *plugins;
126
127 assert(index);
128
129 if (type == LYPLG_EXTENSION) {
130 plugins = &plugins_extensions;
131 } else {
132 plugins = &plugins_types;
133 }
134
135 if (*index == plugins->count) {
136 return NULL;
137 }
138
139 *index += 1;
140 return plugins->objs[*index - 1];
141}
142
143void *
144lyplg_find(enum LYPLG type, const char *module, const char *revision, const char *name)
145{
146 uint32_t i = 0;
147 struct lyplg_record *item;
148
149 assert(module);
150 assert(name);
151
152 while ((item = plugins_iter(type, &i)) != NULL) {
153 if (!strcmp(item->module, module) && !strcmp(item->name, name)) {
154 if (item->revision && revision && strcmp(item->revision, revision)) {
155 continue;
156 } else if (!revision && item->revision) {
157 continue;
158 }
159
160 return &item->plugin;
161 }
162 }
163
164 return NULL;
165}
166
167/**
168 * @brief Insert the provided extension plugin records into the internal set of extension plugins for use by libyang.
169 *
170 * @param[in] recs An array of plugin records provided by the plugin implementation. The array must be terminated by a zeroed
171 * record.
172 * @return LY_SUCCESS in case of success
173 * @return LY_EINVAL for invalid information in @p recs.
174 * @return LY_EMEM in case of memory allocation failure.
175 */
176static LY_ERR
177plugins_insert(enum LYPLG type, const void *recs)
178{
179 if (!recs) {
180 return LY_SUCCESS;
181 }
182
183 if (type == LYPLG_EXTENSION) {
184 const struct lyplg_ext_record *rec = (const struct lyplg_ext_record *)recs;
185
186 for (uint32_t i = 0; rec[i].name; i++) {
187 LY_CHECK_RET(ly_set_add(&plugins_extensions, (void *)&rec[i], 0, NULL));
188 }
Radek Krejcibf940f92021-03-24 21:04:13 +0100189 } else { /* LYPLG_TYPE */
Radek Krejci3e6632f2021-03-22 22:08:21 +0100190 const struct lyplg_type_record *rec = (const struct lyplg_type_record *)recs;
191
192 for (uint32_t i = 0; rec[i].name; i++) {
193 LY_CHECK_RET(ly_set_add(&plugins_types, (void *)&rec[i], 0, NULL));
194 }
195 }
196
197 return LY_SUCCESS;
198}
199
Michal Vasko9e2bc702021-06-09 11:43:36 +0200200#ifndef STATIC
201
Radek Krejci3e6632f2021-03-22 22:08:21 +0100202static void
Michal Vaskoe43a34e2021-04-13 13:43:04 +0200203lyplg_close_cb(void *handle)
204{
205 dlclose(handle);
206}
207
208static void
Radek Krejci3e6632f2021-03-22 22:08:21 +0100209lyplg_clean_(void)
210{
211 if (--context_refcount) {
212 /* there is still some other context, do not remove the plugins */
213 return;
214 }
215
216 ly_set_erase(&plugins_types, NULL);
217 ly_set_erase(&plugins_extensions, NULL);
Michal Vaskoe43a34e2021-04-13 13:43:04 +0200218 ly_set_erase(&plugins_handlers, lyplg_close_cb);
Radek Krejci3e6632f2021-03-22 22:08:21 +0100219}
220
Michal Vasko9e2bc702021-06-09 11:43:36 +0200221#endif
222
Radek Krejci3e6632f2021-03-22 22:08:21 +0100223void
224lyplg_clean(void)
225{
Michal Vasko9e2bc702021-06-09 11:43:36 +0200226#ifndef STATIC
Radek Krejci3e6632f2021-03-22 22:08:21 +0100227 pthread_mutex_lock(&plugins_guard);
228 lyplg_clean_();
229 pthread_mutex_unlock(&plugins_guard);
Michal Vasko9e2bc702021-06-09 11:43:36 +0200230#endif
Radek Krejci3e6632f2021-03-22 22:08:21 +0100231}
232
Michal Vasko9e2bc702021-06-09 11:43:36 +0200233#ifndef STATIC
234
235/**
236 * @brief Just a variadic data to cover extension and type plugins by a single ::plugins_load() function.
237 *
238 * The values are taken from ::LY_PLUGINS_EXTENSIONS and ::LYPLG_TYPES macros.
239 */
240static const struct {
241 const char *id; /**< string identifier: type/extension */
242 const char *apiver_var; /**< expected variable name holding API version value */
243 const char *plugins_var; /**< expected variable name holding plugin records */
244 const char *envdir; /**< environment variable containing directory with the plugins */
245 const char *dir; /**< default directory with the plugins (has less priority than envdir) */
246 uint32_t apiver; /**< expected API version */
247} plugins_load_info[] = {
248 { /* LYPLG_TYPE */
249 .id = "type",
250 .apiver_var = "plugins_types_apiver__",
251 .plugins_var = "plugins_types__",
252 .envdir = "LIBYANG_TYPES_PLUGINS_DIR",
253 .dir = LYPLG_TYPE_DIR,
254 .apiver = LYPLG_TYPE_API_VERSION
255 }, {/* LYPLG_EXTENSION */
256 .id = "extension",
257 .apiver_var = "plugins_extensions_apiver__",
258 .plugins_var = "plugins_extensions__",
259 .envdir = "LIBYANG_EXTENSIONS_PLUGINS_DIR",
260 .dir = LYPLG_EXT_DIR,
261 .apiver = LYPLG_EXT_API_VERSION
262 }
263};
264
Radek Krejcibf940f92021-03-24 21:04:13 +0100265/**
266 * @brief Get the expected plugin objects from the loaded dynamic object and add the defined plugins into the lists of
267 * available extensions/types plugins.
268 *
269 * @param[in] dlhandler Loaded dynamic library handler.
270 * @param[in] pathname Path of the loaded library for logging.
271 * @param[in] type Type of the plugins to get from the dynamic library. Note that a single library can hold both types
272 * and extensions plugins implementations, so this function should be called twice (once for each plugin type) with
273 * different @p type values
274 * @return LY_ERR values.
275 */
276static LY_ERR
277plugins_load(void *dlhandler, const char *pathname, enum LYPLG type)
278{
279 const void *plugins;
280 uint32_t *version;
281
282 /* type plugin */
283 version = dlsym(dlhandler, plugins_load_info[type].apiver_var);
284 if (version) {
285 /* check version ... */
286 if (*version != plugins_load_info[type].apiver) {
287 LOGERR(NULL, LY_EINVAL, "Processing user %s plugin \"%s\" failed, wrong API version - %d expected, %d found.",
288 plugins_load_info[type].id, pathname, plugins_load_info[type].apiver, *version);
289 return LY_EINVAL;
290 }
291
292 /* ... get types plugins information ... */
293 if (!(plugins = dlsym(dlhandler, plugins_load_info[type].plugins_var))) {
294 char *errstr = dlerror();
295 LOGERR(NULL, LY_EINVAL, "Processing user %s plugin \"%s\" failed, missing %s plugins information (%s).",
296 plugins_load_info[type].id, pathname, plugins_load_info[type].id, errstr);
297 return LY_EINVAL;
298 }
299
300 /* ... and load all the types plugins */
301 LY_CHECK_RET(plugins_insert(type, plugins));
302 }
303
304 return LY_SUCCESS;
305}
306
307static LY_ERR
308plugins_load_module(const char *pathname)
309{
310 LY_ERR ret = LY_SUCCESS;
311 void *dlhandler;
312 uint32_t types_count = 0, extensions_count = 0;
313
314 dlerror(); /* Clear any existing error */
315
316 dlhandler = dlopen(pathname, RTLD_NOW);
317 if (!dlhandler) {
318 LOGERR(NULL, LY_ESYS, "Loading \"%s\" as a plugin failed (%s).", pathname, dlerror());
319 return LY_ESYS;
320 }
321
322 if (ly_set_contains(&plugins_handlers, dlhandler, NULL)) {
323 /* the plugin is already loaded */
324 LOGVRB("Plugin \"%s\" already loaded.", pathname);
325
326 /* keep the correct refcount */
327 dlclose(dlhandler);
328 return LY_SUCCESS;
329 }
330
331 /* remember the current plugins lists for recovery */
332 types_count = plugins_types.count;
333 extensions_count = plugins_extensions.count;
334
335 /* type plugin */
336 ret = plugins_load(dlhandler, pathname, LYPLG_TYPE);
337 LY_CHECK_GOTO(ret, error);
338
339 /* extension plugin */
340 ret = plugins_load(dlhandler, pathname, LYPLG_EXTENSION);
341 LY_CHECK_GOTO(ret, error);
342
343 /* remember the dynamic plugin */
344 ret = ly_set_add(&plugins_handlers, dlhandler, 1, NULL);
345 LY_CHECK_GOTO(ret, error);
346
347 return LY_SUCCESS;
348
349error:
350 dlclose(dlhandler);
351
352 /* revert changes in the lists */
353 while (plugins_types.count > types_count) {
354 ly_set_rm_index(&plugins_types, plugins_types.count - 1, NULL);
355 }
356 while (plugins_extensions.count > extensions_count) {
357 ly_set_rm_index(&plugins_extensions, plugins_extensions.count - 1, NULL);
358 }
359
360 return ret;
361}
362
Radek Krejci968d7552021-03-26 20:33:51 +0100363static LY_ERR
364plugins_insert_dir(enum LYPLG type)
365{
366 LY_ERR ret = LY_SUCCESS;
367 const char *pluginsdir;
368 DIR *dir;
369 ly_bool default_dir = 0;
370
371 /* try to get the plugins directory from environment variable */
372 pluginsdir = getenv(plugins_load_info[type].envdir);
373 if (!pluginsdir) {
374 /* remember that we are going to a default dir and do not print warning if the directory doesn't exist */
375 default_dir = 1;
376 pluginsdir = plugins_load_info[type].dir;
377 }
378
379 dir = opendir(pluginsdir);
380 if (!dir) {
381 /* no directory (or no access to it), no extension plugins */
382 if (!default_dir || (errno != ENOENT)) {
383 LOGWRN(NULL, "Failed to open libyang %s plugins directory \"%s\" (%s).", plugins_load_info[type].id,
384 pluginsdir, strerror(errno));
385 }
386 } else {
387 struct dirent *file;
388
389 while ((file = readdir(dir))) {
390 size_t len;
391 char pathname[PATH_MAX];
392
393 /* required format of the filename is *LYPLG_SUFFIX */
394 len = strlen(file->d_name);
395 if ((len < LYPLG_SUFFIX_LEN + 1) || strcmp(&file->d_name[len - LYPLG_SUFFIX_LEN], LYPLG_SUFFIX)) {
396 continue;
397 }
398
399 /* and construct the filepath */
400 snprintf(pathname, PATH_MAX, "%s/%s", pluginsdir, file->d_name);
401
402 ret = plugins_load_module(pathname);
403 if (ret) {
404 break;
405 }
406 }
407 closedir(dir);
408 }
409
410 return ret;
411}
412
Michal Vasko9e2bc702021-06-09 11:43:36 +0200413#endif
414
Radek Krejci3e6632f2021-03-22 22:08:21 +0100415LY_ERR
416lyplg_init(void)
417{
418 LY_ERR ret;
419
420 pthread_mutex_lock(&plugins_guard);
421 /* let only the first context to initiate plugins, but let others wait for finishing the initiation */
422 if (context_refcount++) {
423 /* already initiated */
424 pthread_mutex_unlock(&plugins_guard);
425 return LY_SUCCESS;
426 }
427
428 /* internal types */
429 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_binary), error);
430 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_bits), error);
431 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_boolean), error);
432 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_decimal64), error);
433 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_empty), error);
434 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_enumeration), error);
435 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_identityref), error);
436 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_instanceid), error);
437 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_integer), error);
438 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_leafref), error);
439 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_string), error);
440 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_union), error);
441
Michal Vaskode4a3412021-04-14 15:38:27 +0200442 /* ietf-inet-types */
Michal Vasko3159e782021-05-03 15:12:35 +0200443 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_ipv4_address), error);
Michal Vasko82bf15e2021-05-06 16:01:56 +0200444 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_ipv4_address_no_zone), error);
Michal Vasko7caa3e62021-05-03 14:59:25 +0200445 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_ipv6_address), error);
Michal Vasko18a4a732021-05-06 16:21:44 +0200446 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_ipv6_address_no_zone), error);
Michal Vasko15dc9fa2021-05-03 14:33:05 +0200447 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_ipv4_prefix), error);
448 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_ipv6_prefix), error);
Michal Vasko3e52de52021-04-13 13:45:55 +0200449
Michal Vaskode4a3412021-04-14 15:38:27 +0200450 /* ietf-yang-types */
451 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_date_and_time), error);
Michal Vaskode4a3412021-04-14 15:38:27 +0200452 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_xpath10), error);
453
Michal Vasko7a53c7d2021-08-06 11:28:57 +0200454 /* ietf-netconf-acm */
455 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_TYPE, plugins_node_instanceid), error);
456
Radek Krejci3e6632f2021-03-22 22:08:21 +0100457 /* internal extensions */
458 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_EXTENSION, plugins_metadata), error);
459 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_EXTENSION, plugins_nacm), error);
460 LY_CHECK_GOTO(ret = plugins_insert(LYPLG_EXTENSION, plugins_yangdata), error);
461
Michal Vasko9e2bc702021-06-09 11:43:36 +0200462#ifndef STATIC
Radek Krejci968d7552021-03-26 20:33:51 +0100463 /* external types */
464 LY_CHECK_GOTO(ret = plugins_insert_dir(LYPLG_TYPE), error);
465
466 /* external extensions */
467 LY_CHECK_GOTO(ret = plugins_insert_dir(LYPLG_EXTENSION), error);
Michal Vasko9e2bc702021-06-09 11:43:36 +0200468#endif
Radek Krejci968d7552021-03-26 20:33:51 +0100469
Radek Krejci3e6632f2021-03-22 22:08:21 +0100470 /* initiation done, wake-up possibly waiting threads creating another contexts */
471 pthread_mutex_unlock(&plugins_guard);
472
473 return LY_SUCCESS;
474
475error:
476 /* initiation was not successful - cleanup (and let others to try) */
Michal Vasko9e2bc702021-06-09 11:43:36 +0200477#ifndef STATIC
Radek Krejci3e6632f2021-03-22 22:08:21 +0100478 lyplg_clean_();
Michal Vasko9e2bc702021-06-09 11:43:36 +0200479#endif
Radek Krejci3e6632f2021-03-22 22:08:21 +0100480 pthread_mutex_unlock(&plugins_guard);
481
482 if (ret == LY_EINVAL) {
483 /* all the plugins here are internal, invalid record actually means an internal libyang error */
484 ret = LY_EINT;
485 }
486 return ret;
487}
Radek Krejcibf940f92021-03-24 21:04:13 +0100488
Jan Kundrátc53a7ec2021-12-09 16:01:19 +0100489LIBYANG_API_DEF LY_ERR
Radek Krejcibf940f92021-03-24 21:04:13 +0100490lyplg_add(const char *pathname)
491{
Michal Vasko9e2bc702021-06-09 11:43:36 +0200492#ifdef STATIC
493 (void)pathname;
494
495 LOGERR(NULL, LY_EINVAL, "Plugins are not supported in statically built library.");
496 return LY_EINVAL;
Jan Kundrát323c3122021-12-14 11:44:57 +0100497#elif defined (_WIN32)
498 (void)pathname;
499
500 LOGERR(NULL, LY_EINVAL, "Plugins are not (yet) supported on Windows.");
501 return LY_EINVAL;
Michal Vasko9e2bc702021-06-09 11:43:36 +0200502#else
Radek Krejcibf940f92021-03-24 21:04:13 +0100503 LY_ERR ret = LY_SUCCESS;
504
505 LY_CHECK_ARG_RET(NULL, pathname, LY_EINVAL);
506
507 /* works only in case a context exists */
508 pthread_mutex_lock(&plugins_guard);
509 if (!context_refcount) {
510 /* no context */
511 pthread_mutex_unlock(&plugins_guard);
512 LOGERR(NULL, LY_EDENIED, "To add a plugin, at least one context must exists.");
513 return LY_EDENIED;
514 }
515
516 ret = plugins_load_module(pathname);
517
518 pthread_mutex_unlock(&plugins_guard);
519
520 return ret;
Michal Vasko9e2bc702021-06-09 11:43:36 +0200521#endif
Radek Krejcibf940f92021-03-24 21:04:13 +0100522}