mirror of
https://git.FreeBSD.org/src.git
synced 2024-12-17 10:26:15 +00:00
06a99fe36f
databases. - Make nsswitch support caching. Submitted by: Michael Bushkov <bushman__at__rsu.ru> Sponsored by: Google Summer of Code 2005
589 lines
17 KiB
C
589 lines
17 KiB
C
/*-
|
|
* Copyright (c) 2005 Michael Bushkov <bushman@rsu.ru>
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*
|
|
*/
|
|
|
|
#include <sys/cdefs.h>
|
|
__FBSDID("$FreeBSD$");
|
|
|
|
#include <assert.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "config.h"
|
|
#include "debug.h"
|
|
#include "log.h"
|
|
|
|
/*
|
|
* Default entries, which always exist in the configuration
|
|
*/
|
|
const char *c_default_entries[6] = {
|
|
NSDB_PASSWD,
|
|
NSDB_GROUP,
|
|
NSDB_HOSTS,
|
|
NSDB_SERVICES,
|
|
NSDB_PROTOCOLS,
|
|
NSDB_RPC
|
|
};
|
|
|
|
static int configuration_entry_cmp(const void *, const void *);
|
|
static int configuration_entry_sort_cmp(const void *, const void *);
|
|
static int configuration_entry_cache_mp_sort_cmp(const void *, const void *);
|
|
static int configuration_entry_cache_mp_cmp(const void *, const void *);
|
|
static int configuration_entry_cache_mp_part_cmp(const void *, const void *);
|
|
static struct configuration_entry *create_configuration_entry(const char *,
|
|
struct timeval const *, struct timeval const *,
|
|
struct common_cache_entry_params const *,
|
|
struct common_cache_entry_params const *,
|
|
struct mp_cache_entry_params const *);
|
|
|
|
static int
|
|
configuration_entry_sort_cmp(const void *e1, const void *e2)
|
|
{
|
|
return (strcmp((*((struct configuration_entry **)e1))->name,
|
|
(*((struct configuration_entry **)e2))->name
|
|
));
|
|
}
|
|
|
|
static int
|
|
configuration_entry_cmp(const void *e1, const void *e2)
|
|
{
|
|
return (strcmp((const char *)e1,
|
|
(*((struct configuration_entry **)e2))->name
|
|
));
|
|
}
|
|
|
|
static int
|
|
configuration_entry_cache_mp_sort_cmp(const void *e1, const void *e2)
|
|
{
|
|
return (strcmp((*((cache_entry *)e1))->params->entry_name,
|
|
(*((cache_entry *)e2))->params->entry_name
|
|
));
|
|
}
|
|
|
|
static int
|
|
configuration_entry_cache_mp_cmp(const void *e1, const void *e2)
|
|
{
|
|
return (strcmp((const char *)e1,
|
|
(*((cache_entry *)e2))->params->entry_name
|
|
));
|
|
}
|
|
|
|
static int
|
|
configuration_entry_cache_mp_part_cmp(const void *e1, const void *e2)
|
|
{
|
|
return (strncmp((const char *)e1,
|
|
(*((cache_entry *)e2))->params->entry_name,
|
|
strlen((const char *)e1)
|
|
));
|
|
}
|
|
|
|
static struct configuration_entry *
|
|
create_configuration_entry(const char *name,
|
|
struct timeval const *common_timeout,
|
|
struct timeval const *mp_timeout,
|
|
struct common_cache_entry_params const *positive_params,
|
|
struct common_cache_entry_params const *negative_params,
|
|
struct mp_cache_entry_params const *mp_params)
|
|
{
|
|
struct configuration_entry *retval;
|
|
size_t size;
|
|
int res;
|
|
|
|
TRACE_IN(create_configuration_entry);
|
|
assert(name != NULL);
|
|
assert(positive_params != NULL);
|
|
assert(negative_params != NULL);
|
|
assert(mp_params != NULL);
|
|
|
|
retval = (struct configuration_entry *)malloc(
|
|
sizeof(struct configuration_entry));
|
|
assert(retval != NULL);
|
|
memset(retval, 0, sizeof(struct configuration_entry));
|
|
|
|
res = pthread_mutex_init(&retval->positive_cache_lock, NULL);
|
|
if (res != 0) {
|
|
free(retval);
|
|
LOG_ERR_2("create_configuration_entry",
|
|
"can't create positive cache lock");
|
|
TRACE_OUT(create_configuration_entry);
|
|
return (NULL);
|
|
}
|
|
|
|
res = pthread_mutex_init(&retval->negative_cache_lock, NULL);
|
|
if (res != 0) {
|
|
pthread_mutex_destroy(&retval->positive_cache_lock);
|
|
free(retval);
|
|
LOG_ERR_2("create_configuration_entry",
|
|
"can't create negative cache lock");
|
|
TRACE_OUT(create_configuration_entry);
|
|
return (NULL);
|
|
}
|
|
|
|
res = pthread_mutex_init(&retval->mp_cache_lock, NULL);
|
|
if (res != 0) {
|
|
pthread_mutex_destroy(&retval->positive_cache_lock);
|
|
pthread_mutex_destroy(&retval->negative_cache_lock);
|
|
free(retval);
|
|
LOG_ERR_2("create_configuration_entry",
|
|
"can't create negative cache lock");
|
|
TRACE_OUT(create_configuration_entry);
|
|
return (NULL);
|
|
}
|
|
|
|
memcpy(&retval->positive_cache_params, positive_params,
|
|
sizeof(struct common_cache_entry_params));
|
|
memcpy(&retval->negative_cache_params, negative_params,
|
|
sizeof(struct common_cache_entry_params));
|
|
memcpy(&retval->mp_cache_params, mp_params,
|
|
sizeof(struct mp_cache_entry_params));
|
|
|
|
size = strlen(name);
|
|
retval->name = (char *)malloc(size + 1);
|
|
assert(retval->name != NULL);
|
|
memset(retval->name, 0, size + 1);
|
|
memcpy(retval->name, name, size);
|
|
|
|
memcpy(&retval->common_query_timeout, common_timeout,
|
|
sizeof(struct timeval));
|
|
memcpy(&retval->mp_query_timeout, mp_timeout,
|
|
sizeof(struct timeval));
|
|
|
|
asprintf(&retval->positive_cache_params.entry_name, "%s+", name);
|
|
assert(retval->positive_cache_params.entry_name != NULL);
|
|
|
|
asprintf(&retval->negative_cache_params.entry_name, "%s-", name);
|
|
assert(retval->negative_cache_params.entry_name != NULL);
|
|
|
|
asprintf(&retval->mp_cache_params.entry_name, "%s*", name);
|
|
assert(retval->mp_cache_params.entry_name != NULL);
|
|
|
|
TRACE_OUT(create_configuration_entry);
|
|
return (retval);
|
|
}
|
|
|
|
/*
|
|
* Creates configuration entry and fills it with default values
|
|
*/
|
|
struct configuration_entry *
|
|
create_def_configuration_entry(const char *name)
|
|
{
|
|
struct common_cache_entry_params positive_params, negative_params;
|
|
struct mp_cache_entry_params mp_params;
|
|
struct timeval default_common_timeout, default_mp_timeout;
|
|
|
|
struct configuration_entry *res = NULL;
|
|
|
|
TRACE_IN(create_def_configuration_entry);
|
|
memset(&positive_params, 0,
|
|
sizeof(struct common_cache_entry_params));
|
|
positive_params.entry_type = CET_COMMON;
|
|
positive_params.cache_entries_size = DEFAULT_CACHE_HT_SIZE;
|
|
positive_params.max_elemsize = DEFAULT_POSITIVE_ELEMENTS_SIZE;
|
|
positive_params.satisf_elemsize = DEFAULT_POSITIVE_ELEMENTS_SIZE / 2;
|
|
positive_params.max_lifetime.tv_sec = DEFAULT_POSITIVE_LIFETIME;
|
|
positive_params.policy = CPT_LRU;
|
|
|
|
memcpy(&negative_params, &positive_params,
|
|
sizeof(struct common_cache_entry_params));
|
|
negative_params.max_elemsize = DEFAULT_NEGATIVE_ELEMENTS_SIZE;
|
|
negative_params.satisf_elemsize = DEFAULT_NEGATIVE_ELEMENTS_SIZE / 2;
|
|
negative_params.max_lifetime.tv_sec = DEFAULT_NEGATIVE_LIFETIME;
|
|
negative_params.policy = CPT_FIFO;
|
|
|
|
memset(&default_common_timeout, 0, sizeof(struct timeval));
|
|
default_common_timeout.tv_sec = DEFAULT_COMMON_ENTRY_TIMEOUT;
|
|
|
|
memset(&default_mp_timeout, 0, sizeof(struct timeval));
|
|
default_mp_timeout.tv_sec = DEFAULT_MP_ENTRY_TIMEOUT;
|
|
|
|
memset(&mp_params, 0,
|
|
sizeof(struct mp_cache_entry_params));
|
|
mp_params.entry_type = CET_MULTIPART;
|
|
mp_params.max_elemsize = DEFAULT_MULTIPART_ELEMENTS_SIZE;
|
|
mp_params.max_sessions = DEFAULT_MULITPART_SESSIONS_SIZE;
|
|
mp_params.max_lifetime.tv_sec = DEFAULT_MULITPART_LIFETIME;
|
|
|
|
res = create_configuration_entry(name, &default_common_timeout,
|
|
&default_mp_timeout, &positive_params, &negative_params,
|
|
&mp_params);
|
|
|
|
TRACE_OUT(create_def_configuration_entry);
|
|
return (res);
|
|
}
|
|
|
|
void
|
|
destroy_configuration_entry(struct configuration_entry *entry)
|
|
{
|
|
TRACE_IN(destroy_configuration_entry);
|
|
assert(entry != NULL);
|
|
pthread_mutex_destroy(&entry->positive_cache_lock);
|
|
pthread_mutex_destroy(&entry->negative_cache_lock);
|
|
pthread_mutex_destroy(&entry->mp_cache_lock);
|
|
free(entry->name);
|
|
free(entry->positive_cache_params.entry_name);
|
|
free(entry->negative_cache_params.entry_name);
|
|
free(entry->mp_cache_params.entry_name);
|
|
free(entry->mp_cache_entries);
|
|
free(entry);
|
|
TRACE_OUT(destroy_configuration_entry);
|
|
}
|
|
|
|
int
|
|
add_configuration_entry(struct configuration *config,
|
|
struct configuration_entry *entry)
|
|
{
|
|
TRACE_IN(add_configuration_entry);
|
|
assert(entry != NULL);
|
|
assert(entry->name != NULL);
|
|
if (configuration_find_entry(config, entry->name) != NULL) {
|
|
TRACE_OUT(add_configuration_entry);
|
|
return (-1);
|
|
}
|
|
|
|
if (config->entries_size == config->entries_capacity) {
|
|
struct configuration_entry **new_entries;
|
|
|
|
config->entries_capacity *= 2;
|
|
new_entries = (struct configuration_entry **)malloc(
|
|
sizeof(struct configuration_entry *) *
|
|
config->entries_capacity);
|
|
assert(new_entries != NULL);
|
|
memset(new_entries, 0, sizeof(struct configuration_entry *) *
|
|
config->entries_capacity);
|
|
memcpy(new_entries, config->entries,
|
|
sizeof(struct configuration_entry *) *
|
|
config->entries_size);
|
|
|
|
free(config->entries);
|
|
config->entries = new_entries;
|
|
}
|
|
|
|
config->entries[config->entries_size++] = entry;
|
|
qsort(config->entries, config->entries_size,
|
|
sizeof(struct configuration_entry *),
|
|
configuration_entry_sort_cmp);
|
|
|
|
TRACE_OUT(add_configuration_entry);
|
|
return (0);
|
|
}
|
|
|
|
size_t
|
|
configuration_get_entries_size(struct configuration *config)
|
|
{
|
|
TRACE_IN(configuration_get_entries_size);
|
|
assert(config != NULL);
|
|
TRACE_OUT(configuration_get_entries_size);
|
|
return (config->entries_size);
|
|
}
|
|
|
|
struct configuration_entry *
|
|
configuration_get_entry(struct configuration *config, size_t index)
|
|
{
|
|
TRACE_IN(configuration_get_entry);
|
|
assert(config != NULL);
|
|
assert(index < config->entries_size);
|
|
TRACE_OUT(configuration_get_entry);
|
|
return (config->entries[index]);
|
|
}
|
|
|
|
struct configuration_entry *
|
|
configuration_find_entry(struct configuration *config,
|
|
const char *name)
|
|
{
|
|
struct configuration_entry **retval;
|
|
|
|
TRACE_IN(configuration_find_entry);
|
|
|
|
retval = bsearch(name, config->entries, config->entries_size,
|
|
sizeof(struct configuration_entry *), configuration_entry_cmp);
|
|
TRACE_OUT(configuration_find_entry);
|
|
|
|
return ((retval != NULL) ? *retval : NULL);
|
|
}
|
|
|
|
/*
|
|
* All multipart cache entries are stored in the configuration_entry in the
|
|
* sorted array (sorted by names). The 3 functions below manage this array.
|
|
*/
|
|
|
|
int
|
|
configuration_entry_add_mp_cache_entry(struct configuration_entry *config_entry,
|
|
cache_entry c_entry)
|
|
{
|
|
cache_entry *new_mp_entries, *old_mp_entries;
|
|
|
|
TRACE_IN(configuration_entry_add_mp_cache_entry);
|
|
++config_entry->mp_cache_entries_size;
|
|
new_mp_entries = (cache_entry *)malloc(sizeof(cache_entry) *
|
|
config_entry->mp_cache_entries_size);
|
|
assert(new_mp_entries != NULL);
|
|
new_mp_entries[0] = c_entry;
|
|
|
|
if (config_entry->mp_cache_entries_size - 1 > 0) {
|
|
memcpy(new_mp_entries + 1,
|
|
config_entry->mp_cache_entries,
|
|
(config_entry->mp_cache_entries_size - 1) *
|
|
sizeof(cache_entry));
|
|
}
|
|
|
|
old_mp_entries = config_entry->mp_cache_entries;
|
|
config_entry->mp_cache_entries = new_mp_entries;
|
|
free(old_mp_entries);
|
|
|
|
qsort(config_entry->mp_cache_entries,
|
|
config_entry->mp_cache_entries_size,
|
|
sizeof(cache_entry),
|
|
configuration_entry_cache_mp_sort_cmp);
|
|
|
|
TRACE_OUT(configuration_entry_add_mp_cache_entry);
|
|
return (0);
|
|
}
|
|
|
|
cache_entry
|
|
configuration_entry_find_mp_cache_entry(
|
|
struct configuration_entry *config_entry, const char *mp_name)
|
|
{
|
|
cache_entry *result;
|
|
|
|
TRACE_IN(configuration_entry_find_mp_cache_entry);
|
|
result = bsearch(mp_name, config_entry->mp_cache_entries,
|
|
config_entry->mp_cache_entries_size,
|
|
sizeof(cache_entry), configuration_entry_cache_mp_cmp);
|
|
|
|
if (result == NULL) {
|
|
TRACE_OUT(configuration_entry_find_mp_cache_entry);
|
|
return (NULL);
|
|
} else {
|
|
TRACE_OUT(configuration_entry_find_mp_cache_entry);
|
|
return (*result);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Searches for all multipart entries with names starting with mp_name.
|
|
* Needed for cache flushing.
|
|
*/
|
|
int
|
|
configuration_entry_find_mp_cache_entries(
|
|
struct configuration_entry *config_entry, const char *mp_name,
|
|
cache_entry **start, cache_entry **finish)
|
|
{
|
|
cache_entry *result;
|
|
|
|
TRACE_IN(configuration_entry_find_mp_cache_entries);
|
|
result = bsearch(mp_name, config_entry->mp_cache_entries,
|
|
config_entry->mp_cache_entries_size,
|
|
sizeof(cache_entry), configuration_entry_cache_mp_part_cmp);
|
|
|
|
if (result == NULL) {
|
|
TRACE_OUT(configuration_entry_find_mp_cache_entries);
|
|
return (-1);
|
|
}
|
|
|
|
*start = result;
|
|
*finish = result + 1;
|
|
|
|
while (*start != config_entry->mp_cache_entries) {
|
|
if (configuration_entry_cache_mp_part_cmp(mp_name, *start - 1) == 0)
|
|
*start = *start - 1;
|
|
else
|
|
break;
|
|
}
|
|
|
|
while (*finish != config_entry->mp_cache_entries +
|
|
config_entry->mp_cache_entries_size) {
|
|
|
|
if (configuration_entry_cache_mp_part_cmp(
|
|
mp_name, *finish) == 0)
|
|
*finish = *finish + 1;
|
|
else
|
|
break;
|
|
}
|
|
|
|
TRACE_OUT(configuration_entry_find_mp_cache_entries);
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Configuration entry uses rwlock to handle access to its fields.
|
|
*/
|
|
void
|
|
configuration_lock_rdlock(struct configuration *config)
|
|
{
|
|
TRACE_IN(configuration_lock_rdlock);
|
|
pthread_rwlock_rdlock(&config->rwlock);
|
|
TRACE_OUT(configuration_lock_rdlock);
|
|
}
|
|
|
|
void
|
|
configuration_lock_wrlock(struct configuration *config)
|
|
{
|
|
TRACE_IN(configuration_lock_wrlock);
|
|
pthread_rwlock_wrlock(&config->rwlock);
|
|
TRACE_OUT(configuration_lock_wrlock);
|
|
}
|
|
|
|
void
|
|
configuration_unlock(struct configuration *config)
|
|
{
|
|
TRACE_IN(configuration_unlock);
|
|
pthread_rwlock_unlock(&config->rwlock);
|
|
TRACE_OUT(configuration_unlock);
|
|
}
|
|
|
|
/*
|
|
* Configuration entry uses 3 mutexes to handle cache operations. They are
|
|
* acquired by configuration_lock_entry and configuration_unlock_entry
|
|
* functions.
|
|
*/
|
|
void
|
|
configuration_lock_entry(struct configuration_entry *entry,
|
|
enum config_entry_lock_type lock_type)
|
|
{
|
|
TRACE_IN(configuration_lock_entry);
|
|
assert(entry != NULL);
|
|
|
|
switch (lock_type) {
|
|
case CELT_POSITIVE:
|
|
pthread_mutex_lock(&entry->positive_cache_lock);
|
|
break;
|
|
case CELT_NEGATIVE:
|
|
pthread_mutex_lock(&entry->negative_cache_lock);
|
|
break;
|
|
case CELT_MULTIPART:
|
|
pthread_mutex_lock(&entry->mp_cache_lock);
|
|
break;
|
|
default:
|
|
/* should be unreachable */
|
|
break;
|
|
}
|
|
TRACE_OUT(configuration_lock_entry);
|
|
}
|
|
|
|
void
|
|
configuration_unlock_entry(struct configuration_entry *entry,
|
|
enum config_entry_lock_type lock_type)
|
|
{
|
|
TRACE_IN(configuration_unlock_entry);
|
|
assert(entry != NULL);
|
|
|
|
switch (lock_type) {
|
|
case CELT_POSITIVE:
|
|
pthread_mutex_unlock(&entry->positive_cache_lock);
|
|
break;
|
|
case CELT_NEGATIVE:
|
|
pthread_mutex_unlock(&entry->negative_cache_lock);
|
|
break;
|
|
case CELT_MULTIPART:
|
|
pthread_mutex_unlock(&entry->mp_cache_lock);
|
|
break;
|
|
default:
|
|
/* should be unreachable */
|
|
break;
|
|
}
|
|
TRACE_OUT(configuration_unlock_entry);
|
|
}
|
|
|
|
struct configuration *
|
|
init_configuration(void)
|
|
{
|
|
struct configuration *retval;
|
|
|
|
TRACE_IN(init_configuration);
|
|
retval = (struct configuration *)malloc(sizeof(struct configuration));
|
|
assert(retval != NULL);
|
|
memset(retval, 0, sizeof(struct configuration));
|
|
|
|
retval->entries_capacity = INITIAL_ENTRIES_CAPACITY;
|
|
retval->entries = (struct configuration_entry **)malloc(
|
|
sizeof(struct configuration_entry *) *
|
|
retval->entries_capacity);
|
|
assert(retval->entries != NULL);
|
|
memset(retval->entries, 0, sizeof(struct configuration_entry *) *
|
|
retval->entries_capacity);
|
|
|
|
pthread_rwlock_init(&retval->rwlock, NULL);
|
|
|
|
TRACE_OUT(init_configuration);
|
|
return (retval);
|
|
}
|
|
|
|
void
|
|
fill_configuration_defaults(struct configuration *config)
|
|
{
|
|
size_t len, i;
|
|
|
|
TRACE_IN(fill_configuration_defaults);
|
|
assert(config != NULL);
|
|
|
|
if (config->socket_path != NULL)
|
|
free(config->socket_path);
|
|
|
|
len = strlen(DEFAULT_SOCKET_PATH);
|
|
config->socket_path = (char *)malloc(len + 1);
|
|
assert(config->socket_path != NULL);
|
|
memset(config->socket_path, 0, len + 1);
|
|
memcpy(config->socket_path, DEFAULT_SOCKET_PATH, len);
|
|
|
|
len = strlen(DEFAULT_PIDFILE_PATH);
|
|
config->pidfile_path = (char *)malloc(len + 1);
|
|
assert(config->pidfile_path != NULL);
|
|
memset(config->pidfile_path, 0, len + 1);
|
|
memcpy(config->pidfile_path, DEFAULT_PIDFILE_PATH, len);
|
|
|
|
config->socket_mode = S_IFSOCK | S_IRUSR | S_IWUSR |
|
|
S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
|
|
config->force_unlink = 1;
|
|
|
|
config->query_timeout = DEFAULT_QUERY_TIMEOUT;
|
|
config->threads_num = DEFAULT_THREADS_NUM;
|
|
|
|
for (i = 0; i < config->entries_size; ++i)
|
|
destroy_configuration_entry(config->entries[i]);
|
|
config->entries_size = 0;
|
|
|
|
TRACE_OUT(fill_configuration_defaults);
|
|
}
|
|
|
|
void
|
|
destroy_configuration(struct configuration *config)
|
|
{
|
|
int i;
|
|
TRACE_IN(destroy_configuration);
|
|
assert(config != NULL);
|
|
free(config->pidfile_path);
|
|
free(config->socket_path);
|
|
|
|
for (i = 0; i < config->entries_size; ++i)
|
|
destroy_configuration_entry(config->entries[i]);
|
|
free(config->entries);
|
|
|
|
pthread_rwlock_destroy(&config->rwlock);
|
|
free(config);
|
|
TRACE_OUT(destroy_configuration);
|
|
}
|