d2bab1788d
environ[0] to be more obvious that environ is not NULL before environ[0] is tested. Although I believe the previous code worked, this change improves code maintainability. Reviewed by: ache MFC after: 3 days
686 lines
17 KiB
C
686 lines
17 KiB
C
/*-
|
|
* Copyright (c) 2007-2008 Sean C. Farley <scf@FreeBSD.org>
|
|
* 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,
|
|
* without modification, immediately at the beginning of the file.
|
|
* 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 ``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 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 "namespace.h"
|
|
#include <sys/types.h>
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include "un-namespace.h"
|
|
|
|
|
|
static const char CorruptEnvFindMsg[] = "environment corrupt; unable to find ";
|
|
static const char CorruptEnvValueMsg[] =
|
|
"environment corrupt; missing value for ";
|
|
|
|
|
|
/*
|
|
* Standard environ. environ variable is exposed to entire process.
|
|
*
|
|
* origEnviron: Upon cleanup on unloading of library or failure, this
|
|
* allows environ to return to as it was before.
|
|
* environSize: Number of variables environ can hold. Can only
|
|
* increase.
|
|
* intEnviron: Internally-built environ. Exposed via environ during
|
|
* (re)builds of the environment.
|
|
*/
|
|
extern char **environ;
|
|
static char **origEnviron;
|
|
static char **intEnviron = NULL;
|
|
static int environSize = 0;
|
|
|
|
/*
|
|
* Array of environment variables built from environ. Each element records:
|
|
* name: Pointer to name=value string
|
|
* name length: Length of name not counting '=' character
|
|
* value: Pointer to value within same string as name
|
|
* value size: Size (not length) of space for value not counting the
|
|
* nul character
|
|
* active state: true/false value to signify whether variable is active.
|
|
* Useful since multiple variables with the same name can
|
|
* co-exist. At most, one variable can be active at any
|
|
* one time.
|
|
* putenv: Created from putenv() call. This memory must not be
|
|
* reused.
|
|
*/
|
|
static struct envVars {
|
|
size_t nameLen;
|
|
size_t valueSize;
|
|
char *name;
|
|
char *value;
|
|
bool active;
|
|
bool putenv;
|
|
} *envVars = NULL;
|
|
|
|
/*
|
|
* Environment array information.
|
|
*
|
|
* envActive: Number of active variables in array.
|
|
* envVarsSize: Size of array.
|
|
* envVarsTotal: Number of total variables in array (active or not).
|
|
*/
|
|
static int envActive = 0;
|
|
static int envVarsSize = 0;
|
|
static int envVarsTotal = 0;
|
|
|
|
|
|
/* Deinitialization of new environment. */
|
|
static void __attribute__ ((destructor)) __clean_env_destructor(void);
|
|
|
|
|
|
/*
|
|
* A simple version of warnx() to avoid the bloat of including stdio in static
|
|
* binaries.
|
|
*/
|
|
static void
|
|
__env_warnx(const char *msg, const char *name, size_t nameLen)
|
|
{
|
|
static const char nl[] = "\n";
|
|
static const char progSep[] = ": ";
|
|
|
|
_write(STDERR_FILENO, _getprogname(), strlen(_getprogname()));
|
|
_write(STDERR_FILENO, progSep, sizeof(progSep) - 1);
|
|
_write(STDERR_FILENO, msg, strlen(msg));
|
|
_write(STDERR_FILENO, name, nameLen);
|
|
_write(STDERR_FILENO, nl, sizeof(nl) - 1);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
/*
|
|
* Inline strlen() for performance. Also, perform check for an equals sign.
|
|
* Cheaper here than peforming a strchr() later.
|
|
*/
|
|
static inline size_t
|
|
__strleneq(const char *str)
|
|
{
|
|
const char *s;
|
|
|
|
for (s = str; *s != '\0'; ++s)
|
|
if (*s == '=')
|
|
return (0);
|
|
|
|
return (s - str);
|
|
}
|
|
|
|
|
|
/*
|
|
* Comparison of an environment name=value to a name.
|
|
*/
|
|
static inline bool
|
|
strncmpeq(const char *nameValue, const char *name, size_t nameLen)
|
|
{
|
|
if (strncmp(nameValue, name, nameLen) == 0 && nameValue[nameLen] == '=')
|
|
return (true);
|
|
|
|
return (false);
|
|
}
|
|
|
|
|
|
/*
|
|
* Using environment, returns pointer to value associated with name, if any,
|
|
* else NULL. If the onlyActive flag is set to true, only variables that are
|
|
* active are returned else all are.
|
|
*/
|
|
static inline char *
|
|
__findenv(const char *name, size_t nameLen, int *envNdx, bool onlyActive)
|
|
{
|
|
int ndx;
|
|
|
|
/*
|
|
* Find environment variable from end of array (more likely to be
|
|
* active). A variable created by putenv is always active or it is not
|
|
* tracked in the array.
|
|
*/
|
|
for (ndx = *envNdx; ndx >= 0; ndx--)
|
|
if (envVars[ndx].putenv) {
|
|
if (strncmpeq(envVars[ndx].name, name, nameLen)) {
|
|
*envNdx = ndx;
|
|
return (envVars[ndx].name + nameLen +
|
|
sizeof ("=") - 1);
|
|
}
|
|
} else if ((!onlyActive || envVars[ndx].active) &&
|
|
(envVars[ndx].nameLen == nameLen &&
|
|
strncmpeq(envVars[ndx].name, name, nameLen))) {
|
|
*envNdx = ndx;
|
|
return (envVars[ndx].value);
|
|
}
|
|
|
|
return (NULL);
|
|
}
|
|
|
|
|
|
/*
|
|
* Using environ, returns pointer to value associated with name, if any, else
|
|
* NULL. Used on the original environ passed into the program.
|
|
*/
|
|
static char *
|
|
__findenv_environ(const char *name, size_t nameLen)
|
|
{
|
|
int envNdx;
|
|
|
|
/* Find variable within environ. */
|
|
for (envNdx = 0; environ[envNdx] != NULL; envNdx++)
|
|
if (strncmpeq(environ[envNdx], name, nameLen))
|
|
return (&(environ[envNdx][nameLen + sizeof("=") - 1]));
|
|
|
|
return (NULL);
|
|
}
|
|
|
|
|
|
/*
|
|
* Remove variable added by putenv() from variable tracking array.
|
|
*/
|
|
static void
|
|
__remove_putenv(int envNdx)
|
|
{
|
|
envVarsTotal--;
|
|
if (envVarsTotal > envNdx)
|
|
memmove(&(envVars[envNdx]), &(envVars[envNdx + 1]),
|
|
(envVarsTotal - envNdx) * sizeof (*envVars));
|
|
memset(&(envVars[envVarsTotal]), 0, sizeof (*envVars));
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
/*
|
|
* Deallocate the environment built from environ as well as environ then set
|
|
* both to NULL. Eases debugging of memory leaks.
|
|
*/
|
|
static void
|
|
__clean_env(bool freeVars)
|
|
{
|
|
int envNdx;
|
|
|
|
/* Deallocate environment and environ if created by *env(). */
|
|
if (envVars != NULL) {
|
|
for (envNdx = envVarsTotal - 1; envNdx >= 0; envNdx--)
|
|
/* Free variables or deactivate them. */
|
|
if (envVars[envNdx].putenv) {
|
|
if (!freeVars)
|
|
__remove_putenv(envNdx);
|
|
} else {
|
|
if (freeVars)
|
|
free(envVars[envNdx].name);
|
|
else
|
|
envVars[envNdx].active = false;
|
|
}
|
|
if (freeVars) {
|
|
free(envVars);
|
|
envVars = NULL;
|
|
} else
|
|
envActive = 0;
|
|
|
|
/* Restore original environ if it has not updated by program. */
|
|
if (origEnviron != NULL) {
|
|
if (environ == intEnviron)
|
|
environ = origEnviron;
|
|
free(intEnviron);
|
|
intEnviron = NULL;
|
|
environSize = 0;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
/*
|
|
* Using the environment, rebuild the environ array for use by other C library
|
|
* calls that depend upon it.
|
|
*/
|
|
static int
|
|
__rebuild_environ(int newEnvironSize)
|
|
{
|
|
char **tmpEnviron;
|
|
int envNdx;
|
|
int environNdx;
|
|
int tmpEnvironSize;
|
|
|
|
/* Resize environ. */
|
|
if (newEnvironSize > environSize) {
|
|
tmpEnvironSize = newEnvironSize * 2;
|
|
tmpEnviron = realloc(intEnviron, sizeof (*intEnviron) *
|
|
(tmpEnvironSize + 1));
|
|
if (tmpEnviron == NULL)
|
|
return (-1);
|
|
environSize = tmpEnvironSize;
|
|
intEnviron = tmpEnviron;
|
|
}
|
|
envActive = newEnvironSize;
|
|
|
|
/* Assign active variables to environ. */
|
|
for (envNdx = envVarsTotal - 1, environNdx = 0; envNdx >= 0; envNdx--)
|
|
if (envVars[envNdx].active)
|
|
intEnviron[environNdx++] = envVars[envNdx].name;
|
|
intEnviron[environNdx] = NULL;
|
|
|
|
/* Always set environ which may have been replaced by program. */
|
|
environ = intEnviron;
|
|
|
|
return (0);
|
|
}
|
|
|
|
|
|
/*
|
|
* Enlarge new environment.
|
|
*/
|
|
static inline bool
|
|
__enlarge_env(void)
|
|
{
|
|
int newEnvVarsSize;
|
|
struct envVars *tmpEnvVars;
|
|
|
|
envVarsTotal++;
|
|
if (envVarsTotal > envVarsSize) {
|
|
newEnvVarsSize = envVarsTotal * 2;
|
|
tmpEnvVars = realloc(envVars, sizeof (*envVars) *
|
|
newEnvVarsSize);
|
|
if (tmpEnvVars == NULL) {
|
|
envVarsTotal--;
|
|
return (false);
|
|
}
|
|
envVarsSize = newEnvVarsSize;
|
|
envVars = tmpEnvVars;
|
|
}
|
|
|
|
return (true);
|
|
}
|
|
|
|
|
|
/*
|
|
* Using environ, build an environment for use by standard C library calls.
|
|
*/
|
|
static int
|
|
__build_env(void)
|
|
{
|
|
char **env;
|
|
int activeNdx;
|
|
int envNdx;
|
|
int savedErrno;
|
|
size_t nameLen;
|
|
|
|
/* Check for non-existant environment. */
|
|
if (environ == NULL || environ[0] == NULL)
|
|
return (0);
|
|
|
|
/* Count environment variables. */
|
|
for (env = environ, envVarsTotal = 0; *env != NULL; env++)
|
|
envVarsTotal++;
|
|
envVarsSize = envVarsTotal * 2;
|
|
|
|
/* Create new environment. */
|
|
envVars = calloc(1, sizeof (*envVars) * envVarsSize);
|
|
if (envVars == NULL)
|
|
goto Failure;
|
|
|
|
/* Copy environ values and keep track of them. */
|
|
for (envNdx = envVarsTotal - 1; envNdx >= 0; envNdx--) {
|
|
envVars[envNdx].putenv = false;
|
|
envVars[envNdx].name =
|
|
strdup(environ[envVarsTotal - envNdx - 1]);
|
|
if (envVars[envNdx].name == NULL)
|
|
goto Failure;
|
|
envVars[envNdx].value = strchr(envVars[envNdx].name, '=');
|
|
if (envVars[envNdx].value != NULL) {
|
|
envVars[envNdx].value++;
|
|
envVars[envNdx].valueSize =
|
|
strlen(envVars[envNdx].value);
|
|
} else {
|
|
__env_warnx(CorruptEnvValueMsg, envVars[envNdx].name,
|
|
strlen(envVars[envNdx].name));
|
|
errno = EFAULT;
|
|
goto Failure;
|
|
}
|
|
|
|
/*
|
|
* Find most current version of variable to make active. This
|
|
* will prevent multiple active variables from being created
|
|
* during this initialization phase.
|
|
*/
|
|
nameLen = envVars[envNdx].value - envVars[envNdx].name - 1;
|
|
envVars[envNdx].nameLen = nameLen;
|
|
activeNdx = envVarsTotal - 1;
|
|
if (__findenv(envVars[envNdx].name, nameLen, &activeNdx,
|
|
false) == NULL) {
|
|
__env_warnx(CorruptEnvFindMsg, envVars[envNdx].name,
|
|
nameLen);
|
|
errno = EFAULT;
|
|
goto Failure;
|
|
}
|
|
envVars[activeNdx].active = true;
|
|
}
|
|
|
|
/* Create a new environ. */
|
|
origEnviron = environ;
|
|
environ = NULL;
|
|
if (__rebuild_environ(envVarsTotal) == 0)
|
|
return (0);
|
|
|
|
Failure:
|
|
savedErrno = errno;
|
|
__clean_env(true);
|
|
errno = savedErrno;
|
|
|
|
return (-1);
|
|
}
|
|
|
|
|
|
/*
|
|
* Destructor function with default argument to __clean_env().
|
|
*/
|
|
static void
|
|
__clean_env_destructor(void)
|
|
{
|
|
__clean_env(true);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the value of a variable or NULL if none are found.
|
|
*/
|
|
char *
|
|
getenv(const char *name)
|
|
{
|
|
int envNdx;
|
|
size_t nameLen;
|
|
|
|
/* Check for malformed name. */
|
|
if (name == NULL || (nameLen = __strleneq(name)) == 0) {
|
|
errno = EINVAL;
|
|
return (NULL);
|
|
}
|
|
|
|
/*
|
|
* An empty environment (environ or its first value) regardless if
|
|
* environ has been copied before will return a NULL.
|
|
*
|
|
* If the environment is not empty, find an environment variable via
|
|
* environ if environ has not been copied via an *env() call or been
|
|
* replaced by a running program, otherwise, use the rebuilt
|
|
* environment.
|
|
*/
|
|
if (environ == NULL || environ[0] == NULL)
|
|
return (NULL);
|
|
else if (envVars == NULL || environ != intEnviron)
|
|
return (__findenv_environ(name, nameLen));
|
|
else {
|
|
envNdx = envVarsTotal - 1;
|
|
return (__findenv(name, nameLen, &envNdx, true));
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Set the value of a variable. Older settings are labeled as inactive. If an
|
|
* older setting has enough room to store the new value, it will be reused. No
|
|
* previous variables are ever freed here to avoid causing a segmentation fault
|
|
* in a user's code.
|
|
*
|
|
* The variables nameLen and valueLen are passed into here to allow the caller
|
|
* to calculate the length by means besides just strlen().
|
|
*/
|
|
static int
|
|
__setenv(const char *name, size_t nameLen, const char *value, int overwrite)
|
|
{
|
|
bool reuse;
|
|
char *env;
|
|
int envNdx;
|
|
int newEnvActive;
|
|
size_t valueLen;
|
|
|
|
/* Find existing environment variable large enough to use. */
|
|
envNdx = envVarsTotal - 1;
|
|
newEnvActive = envActive;
|
|
valueLen = strlen(value);
|
|
reuse = false;
|
|
if (__findenv(name, nameLen, &envNdx, false) != NULL) {
|
|
/* Deactivate entry if overwrite is allowed. */
|
|
if (envVars[envNdx].active) {
|
|
if (overwrite == 0)
|
|
return (0);
|
|
envVars[envNdx].active = false;
|
|
newEnvActive--;
|
|
}
|
|
|
|
/* putenv() created variable cannot be reused. */
|
|
if (envVars[envNdx].putenv)
|
|
__remove_putenv(envNdx);
|
|
|
|
/* Entry is large enough to reuse. */
|
|
else if (envVars[envNdx].valueSize >= valueLen)
|
|
reuse = true;
|
|
}
|
|
|
|
/* Create new variable if none was found of sufficient size. */
|
|
if (! reuse) {
|
|
/* Enlarge environment. */
|
|
envNdx = envVarsTotal;
|
|
if (!__enlarge_env())
|
|
return (-1);
|
|
|
|
/* Create environment entry. */
|
|
envVars[envNdx].name = malloc(nameLen + sizeof ("=") +
|
|
valueLen);
|
|
if (envVars[envNdx].name == NULL) {
|
|
envVarsTotal--;
|
|
return (-1);
|
|
}
|
|
envVars[envNdx].nameLen = nameLen;
|
|
envVars[envNdx].valueSize = valueLen;
|
|
|
|
/* Save name of name/value pair. */
|
|
env = stpcpy(envVars[envNdx].name, name);
|
|
if ((envVars[envNdx].name)[nameLen] != '=')
|
|
env = stpcpy(env, "=");
|
|
}
|
|
else
|
|
env = envVars[envNdx].value;
|
|
|
|
/* Save value of name/value pair. */
|
|
strcpy(env, value);
|
|
envVars[envNdx].value = env;
|
|
envVars[envNdx].active = true;
|
|
newEnvActive++;
|
|
|
|
/* No need to rebuild environ if an active variable was reused. */
|
|
if (reuse && newEnvActive == envActive)
|
|
return (0);
|
|
else
|
|
return (__rebuild_environ(newEnvActive));
|
|
}
|
|
|
|
|
|
/*
|
|
* If the program attempts to replace the array of environment variables
|
|
* (environ) environ or sets the first varible to NULL, then deactivate all
|
|
* variables and merge in the new list from environ.
|
|
*/
|
|
static int
|
|
__merge_environ(void)
|
|
{
|
|
char **env;
|
|
char *equals;
|
|
|
|
/*
|
|
* Internally-built environ has been replaced or cleared (detected by
|
|
* using the count of active variables against a NULL as the first value
|
|
* in environ). Clean up everything.
|
|
*/
|
|
if (intEnviron != NULL && (environ != intEnviron || (envActive > 0 &&
|
|
environ[0] == NULL))) {
|
|
/* Deactivate all environment variables. */
|
|
if (envActive > 0) {
|
|
origEnviron = NULL;
|
|
__clean_env(false);
|
|
}
|
|
|
|
/*
|
|
* Insert new environ into existing, yet deactivated,
|
|
* environment array.
|
|
*/
|
|
origEnviron = environ;
|
|
if (origEnviron != NULL)
|
|
for (env = origEnviron; *env != NULL; env++) {
|
|
if ((equals = strchr(*env, '=')) == NULL) {
|
|
__env_warnx(CorruptEnvValueMsg, *env,
|
|
strlen(*env));
|
|
errno = EFAULT;
|
|
return (-1);
|
|
}
|
|
if (__setenv(*env, equals - *env, equals + 1,
|
|
1) == -1)
|
|
return (-1);
|
|
}
|
|
}
|
|
|
|
return (0);
|
|
}
|
|
|
|
|
|
/*
|
|
* The exposed setenv() that peforms a few tests before calling the function
|
|
* (__setenv()) that does the actual work of inserting a variable into the
|
|
* environment.
|
|
*/
|
|
int
|
|
setenv(const char *name, const char *value, int overwrite)
|
|
{
|
|
size_t nameLen;
|
|
|
|
/* Check for malformed name. */
|
|
if (name == NULL || (nameLen = __strleneq(name)) == 0) {
|
|
errno = EINVAL;
|
|
return (-1);
|
|
}
|
|
|
|
/* Initialize environment. */
|
|
if (__merge_environ() == -1 || (envVars == NULL && __build_env() == -1))
|
|
return (-1);
|
|
|
|
return (__setenv(name, nameLen, value, overwrite));
|
|
}
|
|
|
|
|
|
/*
|
|
* Insert a "name=value" string into the environment. Special settings must be
|
|
* made to keep setenv() from reusing this memory block and unsetenv() from
|
|
* allowing it to be tracked.
|
|
*/
|
|
int
|
|
putenv(char *string)
|
|
{
|
|
char *equals;
|
|
int envNdx;
|
|
int newEnvActive;
|
|
size_t nameLen;
|
|
|
|
/* Check for malformed argument. */
|
|
if (string == NULL || (equals = strchr(string, '=')) == NULL ||
|
|
(nameLen = equals - string) == 0) {
|
|
errno = EINVAL;
|
|
return (-1);
|
|
}
|
|
|
|
/* Initialize environment. */
|
|
if (__merge_environ() == -1 || (envVars == NULL && __build_env() == -1))
|
|
return (-1);
|
|
|
|
/* Deactivate previous environment variable. */
|
|
envNdx = envVarsTotal - 1;
|
|
newEnvActive = envActive;
|
|
if (__findenv(string, nameLen, &envNdx, true) != NULL) {
|
|
/* Reuse previous putenv slot. */
|
|
if (envVars[envNdx].putenv) {
|
|
envVars[envNdx].name = string;
|
|
return (__rebuild_environ(envActive));
|
|
} else {
|
|
newEnvActive--;
|
|
envVars[envNdx].active = false;
|
|
}
|
|
}
|
|
|
|
/* Enlarge environment. */
|
|
envNdx = envVarsTotal;
|
|
if (!__enlarge_env())
|
|
return (-1);
|
|
|
|
/* Create environment entry. */
|
|
envVars[envNdx].name = string;
|
|
envVars[envNdx].nameLen = -1;
|
|
envVars[envNdx].value = NULL;
|
|
envVars[envNdx].valueSize = -1;
|
|
envVars[envNdx].putenv = true;
|
|
envVars[envNdx].active = true;
|
|
newEnvActive++;
|
|
|
|
return (__rebuild_environ(newEnvActive));
|
|
}
|
|
|
|
|
|
/*
|
|
* Unset variable with the same name by flagging it as inactive. No variable is
|
|
* ever freed.
|
|
*/
|
|
int
|
|
unsetenv(const char *name)
|
|
{
|
|
int envNdx;
|
|
size_t nameLen;
|
|
|
|
/* Check for malformed name. */
|
|
if (name == NULL || (nameLen = __strleneq(name)) == 0) {
|
|
errno = EINVAL;
|
|
return (-1);
|
|
}
|
|
|
|
/* Initialize environment. */
|
|
if (__merge_environ() == -1 || (envVars == NULL && __build_env() == -1))
|
|
return (-1);
|
|
|
|
/* Deactivate specified variable. */
|
|
envNdx = envVarsTotal - 1;
|
|
if (__findenv(name, nameLen, &envNdx, true) != NULL) {
|
|
envVars[envNdx].active = false;
|
|
if (envVars[envNdx].putenv)
|
|
__remove_putenv(envNdx);
|
|
__rebuild_environ(envActive - 1);
|
|
}
|
|
|
|
return (0);
|
|
}
|