freebsd-skq/sys/dev/bhnd/nvram/bhnd_nvram_data_bcm.c
landonf 55e6f51da0 bhnd(4): support direct conversion of bhnd_nvram_val
This adds support for bhnd_nvram_val_convert_init() and
bhnd_nvram_val_convert_new(), which may be used to perform value
format-aware encoding of an NVRAM value to a new target format/type.

This will be used to simplify converting to/from serialized
format-specific NVRAM value representations to common external
representations.

Approved by:	adrian (mentor)
Differential Revision:	https://reviews.freebsd.org/D8757
2016-12-19 20:20:33 +00:00

747 lines
19 KiB
C

/*-
* Copyright (c) 2016 Landon Fuller <landonf@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.
* 2. Redistributions in binary form must reproduce at minimum a disclaimer
* similar to the "NO WARRANTY" disclaimer below ("Disclaimer") and any
* redistribution must be conditioned upon including a substantially
* similar Disclaimer requirement for further binary redistribution.
*
* NO WARRANTY
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF NONINFRINGEMENT, MERCHANTIBILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR 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 DAMAGES.
*/
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
#include <sys/param.h>
#include <sys/endian.h>
#ifdef _KERNEL
#include <sys/bus.h>
#include <sys/ctype.h>
#include <sys/malloc.h>
#include <sys/systm.h>
#else /* !_KERNEL */
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#endif /* _KERNEL */
#include "bhnd_nvram_private.h"
#include "bhnd_nvram_datavar.h"
#include "bhnd_nvram_data_bcmreg.h"
#include "bhnd_nvram_data_bcmvar.h"
/*
* Broadcom NVRAM data class.
*
* The Broadcom NVRAM NUL-delimited ASCII format is used by most
* Broadcom SoCs.
*
* The NVRAM data is encoded as a standard header, followed by series of
* NUL-terminated 'key=value' strings; the end of the stream is denoted
* by a single extra NUL character.
*/
struct bhnd_nvram_bcm;
static struct bhnd_nvram_bcm_hvar *bhnd_nvram_bcm_gethdrvar(
struct bhnd_nvram_bcm *bcm,
const char *name);
static struct bhnd_nvram_bcm_hvar *bhnd_nvram_bcm_to_hdrvar(
struct bhnd_nvram_bcm *bcm,
void *cookiep);
static size_t bhnd_nvram_bcm_hdrvar_index(
struct bhnd_nvram_bcm *bcm,
struct bhnd_nvram_bcm_hvar *hvar);
/*
* Set of BCM NVRAM header values that are required to be mirrored in the
* NVRAM data itself.
*
* If they're not included in the parsed NVRAM data, we need to vend the
* header-parsed values with their appropriate keys, and add them in any
* updates to the NVRAM data.
*
* If they're modified in NVRAM, we need to sync the changes with the
* the NVRAM header values.
*/
static const struct bhnd_nvram_bcm_hvar bhnd_nvram_bcm_hvars[] = {
{
.name = BCM_NVRAM_CFG0_SDRAM_INIT_VAR,
.type = BHND_NVRAM_TYPE_UINT16,
.len = sizeof(uint16_t),
.nelem = 1,
},
{
.name = BCM_NVRAM_CFG1_SDRAM_CFG_VAR,
.type = BHND_NVRAM_TYPE_UINT16,
.len = sizeof(uint16_t),
.nelem = 1,
},
{
.name = BCM_NVRAM_CFG1_SDRAM_REFRESH_VAR,
.type = BHND_NVRAM_TYPE_UINT16,
.len = sizeof(uint16_t),
.nelem = 1,
},
{
.name = BCM_NVRAM_SDRAM_NCDL_VAR,
.type = BHND_NVRAM_TYPE_UINT32,
.len = sizeof(uint32_t),
.nelem = 1,
},
};
/** BCM NVRAM data class instance */
struct bhnd_nvram_bcm {
struct bhnd_nvram_data nv; /**< common instance state */
struct bhnd_nvram_io *data; /**< backing buffer */
/** BCM header values */
struct bhnd_nvram_bcm_hvar hvars[nitems(bhnd_nvram_bcm_hvars)];
size_t count; /**< total variable count */
};
BHND_NVRAM_DATA_CLASS_DEFN(bcm, "Broadcom", sizeof(struct bhnd_nvram_bcm))
static int
bhnd_nvram_bcm_probe(struct bhnd_nvram_io *io)
{
struct bhnd_nvram_bcmhdr hdr;
int error;
if ((error = bhnd_nvram_io_read(io, 0x0, &hdr, sizeof(hdr))))
return (error);
if (le32toh(hdr.magic) != BCM_NVRAM_MAGIC)
return (ENXIO);
return (BHND_NVRAM_DATA_PROBE_DEFAULT);
}
/**
* Initialize @p bcm with the provided NVRAM data mapped by @p src.
*
* @param bcm A newly allocated data instance.
*/
static int
bhnd_nvram_bcm_init(struct bhnd_nvram_bcm *bcm, struct bhnd_nvram_io *src)
{
struct bhnd_nvram_bcmhdr hdr;
uint8_t *p;
void *ptr;
size_t io_offset, io_size;
uint8_t crc, valid;
int error;
if ((error = bhnd_nvram_io_read(src, 0x0, &hdr, sizeof(hdr))))
return (error);
if (le32toh(hdr.magic) != BCM_NVRAM_MAGIC)
return (ENXIO);
/* Fetch the actual NVRAM image size */
io_size = le32toh(hdr.size);
if (io_size < sizeof(hdr)) {
/* The header size must include the header itself */
BHND_NV_LOG("corrupt header size: %zu\n", io_size);
return (EINVAL);
}
if (io_size > bhnd_nvram_io_getsize(src)) {
BHND_NV_LOG("header size %zu exceeds input size %zu\n",
io_size, bhnd_nvram_io_getsize(src));
return (EINVAL);
}
/* Allocate a buffer large enough to hold the NVRAM image, and
* an extra EOF-signaling NUL (on the chance it's missing from the
* source data) */
if (io_size == SIZE_MAX)
return (ENOMEM);
bcm->data = bhnd_nvram_iobuf_empty(io_size, io_size + 1);
if (bcm->data == NULL)
return (ENOMEM);
/* Fetch a pointer into our backing buffer and copy in the
* NVRAM image. */
error = bhnd_nvram_io_write_ptr(bcm->data, 0x0, &ptr, io_size, NULL);
if (error)
return (error);
p = ptr;
if ((error = bhnd_nvram_io_read(src, 0x0, p, io_size)))
return (error);
/* Verify the CRC */
valid = BCM_NVRAM_GET_BITS(hdr.cfg0, BCM_NVRAM_CFG0_CRC);
crc = bhnd_nvram_crc8(p + BCM_NVRAM_CRC_SKIP,
io_size - BCM_NVRAM_CRC_SKIP, BHND_NVRAM_CRC8_INITIAL);
if (crc != valid) {
BHND_NV_LOG("warning: NVRAM CRC error (crc=%#hhx, "
"expected=%hhx)\n", crc, valid);
}
/* Populate header variable definitions */
#define BCM_READ_HDR_VAR(_name, _dest, _swap) do { \
struct bhnd_nvram_bcm_hvar *data; \
data = bhnd_nvram_bcm_gethdrvar(bcm, _name ##_VAR); \
BHND_NV_ASSERT(data != NULL, \
("no such header variable: " __STRING(_name))); \
\
\
data->value. _dest = _swap(BCM_NVRAM_GET_BITS( \
hdr. _name ## _FIELD, _name)); \
} while(0)
BCM_READ_HDR_VAR(BCM_NVRAM_CFG0_SDRAM_INIT, u16, le16toh);
BCM_READ_HDR_VAR(BCM_NVRAM_CFG1_SDRAM_CFG, u16, le16toh);
BCM_READ_HDR_VAR(BCM_NVRAM_CFG1_SDRAM_REFRESH, u16, le16toh);
BCM_READ_HDR_VAR(BCM_NVRAM_SDRAM_NCDL, u32, le32toh);
_Static_assert(nitems(bcm->hvars) == 4, "missing initialization for"
"NVRAM header variable(s)");
#undef BCM_READ_HDR_VAR
/* Process the buffer */
bcm->count = 0;
io_offset = sizeof(hdr);
while (io_offset < io_size) {
char *envp;
const char *name, *value;
size_t envp_len;
size_t name_len, value_len;
/* Parse the key=value string */
envp = (char *) (p + io_offset);
envp_len = strnlen(envp, io_size - io_offset);
error = bhnd_nvram_parse_env(envp, envp_len, '=', &name,
&name_len, &value, &value_len);
if (error) {
BHND_NV_LOG("error parsing envp at offset %#zx: %d\n",
io_offset, error);
return (error);
}
/* Insert a '\0' character, replacing the '=' delimiter and
* allowing us to vend references directly to the variable
* name */
*(envp + name_len) = '\0';
/* Record any NVRAM variables that mirror our header variables.
* This is a brute-force search -- for the amount of data we're
* operating on, it shouldn't be an issue. */
for (size_t i = 0; i < nitems(bcm->hvars); i++) {
struct bhnd_nvram_bcm_hvar *hvar;
union bhnd_nvram_bcm_hvar_value hval;
size_t hval_len;
hvar = &bcm->hvars[i];
/* Already matched? */
if (hvar->envp != NULL)
continue;
/* Name matches? */
if ((strcmp(name, hvar->name)) != 0)
continue;
/* Save pointer to mirrored envp */
hvar->envp = envp;
/* Check for stale value */
hval_len = sizeof(hval);
error = bhnd_nvram_value_coerce(value, value_len,
BHND_NVRAM_TYPE_STRING, &hval, &hval_len,
hvar->type);
if (error) {
/* If parsing fails, we can likely only make
* things worse by trying to synchronize the
* variables */
BHND_NV_LOG("error parsing header variable "
"'%s=%s': %d\n", name, value, error);
} else if (hval_len != hvar->len) {
hvar->stale = true;
} else if (memcmp(&hval, &hvar->value, hval_len) != 0) {
hvar->stale = true;
}
}
/* Seek past the value's terminating '\0' */
io_offset += envp_len;
if (io_offset == io_size) {
BHND_NV_LOG("missing terminating NUL at offset %#zx\n",
io_offset);
return (EINVAL);
}
if (*(p + io_offset) != '\0') {
BHND_NV_LOG("invalid terminator '%#hhx' at offset "
"%#zx\n", *(p + io_offset), io_offset);
return (EINVAL);
}
/* Update variable count */
bcm->count++;
/* Seek to the next record */
if (++io_offset == io_size) {
char ch;
/* Hit EOF without finding a terminating NUL
* byte; we need to grow our buffer and append
* it */
io_size++;
if ((error = bhnd_nvram_io_setsize(bcm->data, io_size)))
return (error);
/* Write NUL byte */
ch = '\0';
error = bhnd_nvram_io_write(bcm->data, io_size-1, &ch,
sizeof(ch));
if (error)
return (error);
}
/* Check for explicit EOF (encoded as a single empty NUL
* terminated string) */
if (*(p + io_offset) == '\0')
break;
}
/* Add non-mirrored header variables to total count variable */
for (size_t i = 0; i < nitems(bcm->hvars); i++) {
if (bcm->hvars[i].envp == NULL)
bcm->count++;
}
return (0);
}
static int
bhnd_nvram_bcm_new(struct bhnd_nvram_data *nv, struct bhnd_nvram_io *io)
{
struct bhnd_nvram_bcm *bcm;
int error;
bcm = (struct bhnd_nvram_bcm *)nv;
/* Populate default BCM mirrored header variable set */
_Static_assert(sizeof(bcm->hvars) == sizeof(bhnd_nvram_bcm_hvars),
"hvar declarations must match bhnd_nvram_bcm_hvars template");
memcpy(bcm->hvars, bhnd_nvram_bcm_hvars, sizeof(bcm->hvars));
/* Parse the BCM input data and initialize our backing
* data representation */
if ((error = bhnd_nvram_bcm_init(bcm, io))) {
bhnd_nvram_bcm_free(nv);
return (error);
}
return (0);
}
static void
bhnd_nvram_bcm_free(struct bhnd_nvram_data *nv)
{
struct bhnd_nvram_bcm *bcm = (struct bhnd_nvram_bcm *)nv;
if (bcm->data != NULL)
bhnd_nvram_io_free(bcm->data);
}
size_t
bhnd_nvram_bcm_count(struct bhnd_nvram_data *nv)
{
struct bhnd_nvram_bcm *bcm = (struct bhnd_nvram_bcm *)nv;
return (bcm->count);
}
static int
bhnd_nvram_bcm_size(struct bhnd_nvram_data *nv, size_t *size)
{
return (bhnd_nvram_bcm_serialize(nv, NULL, size));
}
static int
bhnd_nvram_bcm_serialize(struct bhnd_nvram_data *nv, void *buf, size_t *len)
{
struct bhnd_nvram_bcm *bcm;
struct bhnd_nvram_bcmhdr hdr;
void *cookiep;
const char *name;
size_t nbytes, limit;
uint8_t crc;
int error;
bcm = (struct bhnd_nvram_bcm *)nv;
nbytes = 0;
/* Save the output buffer limit */
if (buf == NULL)
limit = 0;
else
limit = *len;
/* Reserve space for the NVRAM header */
nbytes += sizeof(struct bhnd_nvram_bcmhdr);
/* Write all variables to the output buffer */
cookiep = NULL;
while ((name = bhnd_nvram_data_next(nv, &cookiep))) {
uint8_t *outp;
size_t olen;
size_t name_len, val_len;
if (limit > nbytes) {
outp = (uint8_t *)buf + nbytes;
olen = limit - nbytes;
} else {
outp = NULL;
olen = 0;
}
/* Determine length of variable name */
name_len = strlen(name) + 1;
/* Write the variable name and '=' delimiter */
if (olen >= name_len) {
/* Copy name */
memcpy(outp, name, name_len - 1);
/* Append '=' */
*(outp + name_len - 1) = '=';
}
/* Adjust byte counts */
if (SIZE_MAX - name_len < nbytes)
return (ERANGE);
nbytes += name_len;
/* Reposition output */
if (limit > nbytes) {
outp = (uint8_t *)buf + nbytes;
olen = limit - nbytes;
} else {
outp = NULL;
olen = 0;
}
/* Coerce to NUL-terminated C string, writing to the output
* buffer (or just calculating the length if outp is NULL) */
val_len = olen;
error = bhnd_nvram_data_getvar(nv, cookiep, outp, &val_len,
BHND_NVRAM_TYPE_STRING);
if (error && error != ENOMEM)
return (error);
/* Adjust byte counts */
if (SIZE_MAX - val_len < nbytes)
return (ERANGE);
nbytes += val_len;
}
/* Write terminating NUL */
if (nbytes < limit)
*((uint8_t *)buf + nbytes) = '\0';
nbytes++;
/* Provide actual size */
*len = nbytes;
if (buf == NULL || nbytes > limit) {
if (buf != NULL)
return (ENOMEM);
return (0);
}
/* Fetch current NVRAM header */
if ((error = bhnd_nvram_io_read(bcm->data, 0x0, &hdr, sizeof(hdr))))
return (error);
/* Update values covered by CRC and write to output buffer */
hdr.size = htole32(*len);
memcpy(buf, &hdr, sizeof(hdr));
/* Calculate new CRC */
crc = bhnd_nvram_crc8((uint8_t *)buf + BCM_NVRAM_CRC_SKIP,
*len - BCM_NVRAM_CRC_SKIP, BHND_NVRAM_CRC8_INITIAL);
/* Update header with valid CRC */
hdr.cfg0 &= ~BCM_NVRAM_CFG0_CRC_MASK;
hdr.cfg0 |= (crc << BCM_NVRAM_CFG0_CRC_SHIFT);
memcpy(buf, &hdr, sizeof(hdr));
return (0);
}
static uint32_t
bhnd_nvram_bcm_caps(struct bhnd_nvram_data *nv)
{
return (BHND_NVRAM_DATA_CAP_READ_PTR|BHND_NVRAM_DATA_CAP_DEVPATHS);
}
static const char *
bhnd_nvram_bcm_next(struct bhnd_nvram_data *nv, void **cookiep)
{
struct bhnd_nvram_bcm *bcm;
struct bhnd_nvram_bcm_hvar *hvar, *hvar_next;
const void *ptr;
const char *envp, *basep;
size_t io_size, io_offset;
int error;
bcm = (struct bhnd_nvram_bcm *)nv;
io_offset = sizeof(struct bhnd_nvram_bcmhdr);
io_size = bhnd_nvram_io_getsize(bcm->data) - io_offset;
/* Map backing buffer */
error = bhnd_nvram_io_read_ptr(bcm->data, io_offset, &ptr, io_size,
NULL);
if (error) {
BHND_NV_LOG("error mapping backing buffer: %d\n", error);
return (NULL);
}
basep = ptr;
/* If cookiep pointers into our header variable array, handle as header
* variable iteration. */
hvar = bhnd_nvram_bcm_to_hdrvar(bcm, *cookiep);
if (hvar != NULL) {
size_t idx;
/* Advance to next entry, if any */
idx = bhnd_nvram_bcm_hdrvar_index(bcm, hvar) + 1;
/* Find the next header-defined variable that isn't defined in
* the NVRAM data, start iteration there */
for (size_t i = idx; i < nitems(bcm->hvars); i++) {
hvar_next = &bcm->hvars[i];
if (hvar_next->envp != NULL && !hvar_next->stale)
continue;
*cookiep = hvar_next;
return (hvar_next->name);
}
/* No further header-defined variables; iteration
* complete */
return (NULL);
}
/* Handle standard NVRAM data iteration */
if (*cookiep == NULL) {
/* Start at the first NVRAM data record */
envp = basep;
} else {
/* Seek to next record */
envp = *cookiep;
envp += strlen(envp) + 1; /* key + '\0' */
envp += strlen(envp) + 1; /* value + '\0' */
}
/*
* Skip entries that have an existing header variable entry that takes
* precedence over the NVRAM data value.
*
* The header's value will be provided when performing header variable
* iteration
*/
while ((size_t)(envp - basep) < io_size && *envp != '\0') {
/* Locate corresponding header variable */
hvar = NULL;
for (size_t i = 0; i < nitems(bcm->hvars); i++) {
if (bcm->hvars[i].envp != envp)
continue;
hvar = &bcm->hvars[i];
break;
}
/* If no corresponding hvar entry, or the entry does not take
* precedence over this NVRAM value, we can safely return this
* value as-is. */
if (hvar == NULL || !hvar->stale)
break;
/* Seek to next record */
envp += strlen(envp) + 1; /* key + '\0' */
envp += strlen(envp) + 1; /* value + '\0' */
}
/* On NVRAM data EOF, try switching to header variables */
if ((size_t)(envp - basep) == io_size || *envp == '\0') {
/* Find first valid header variable */
for (size_t i = 0; i < nitems(bcm->hvars); i++) {
if (bcm->hvars[i].envp != NULL)
continue;
*cookiep = &bcm->hvars[i];
return (bcm->hvars[i].name);
}
/* No header variables */
return (NULL);
}
*cookiep = (void *)(uintptr_t)envp;
return (envp);
}
static void *
bhnd_nvram_bcm_find(struct bhnd_nvram_data *nv, const char *name)
{
return (bhnd_nvram_data_generic_find(nv, name));
}
static int
bhnd_nvram_bcm_getvar(struct bhnd_nvram_data *nv, void *cookiep, void *buf,
size_t *len, bhnd_nvram_type type)
{
return (bhnd_nvram_data_generic_rp_getvar(nv, cookiep, buf, len, type));
}
static const void *
bhnd_nvram_bcm_getvar_ptr(struct bhnd_nvram_data *nv, void *cookiep,
size_t *len, bhnd_nvram_type *type)
{
struct bhnd_nvram_bcm *bcm;
struct bhnd_nvram_bcm_hvar *hvar;
const char *envp;
bcm = (struct bhnd_nvram_bcm *)nv;
/* Handle header variables */
if ((hvar = bhnd_nvram_bcm_to_hdrvar(bcm, cookiep)) != NULL) {
BHND_NV_ASSERT(bhnd_nvram_value_check_aligned(&hvar->value,
hvar->len, hvar->type) == 0, ("value misaligned"));
*type = hvar->type;
*len = hvar->len;
return (&hvar->value);
}
/* Cookie points to key\0value\0 -- get the value address */
BHND_NV_ASSERT(cookiep != NULL, ("NULL cookiep"));
envp = cookiep;
envp += strlen(envp) + 1; /* key + '\0' */
*len = strlen(envp) + 1; /* value + '\0' */
*type = BHND_NVRAM_TYPE_STRING;
return (envp);
}
static const char *
bhnd_nvram_bcm_getvar_name(struct bhnd_nvram_data *nv, void *cookiep)
{
struct bhnd_nvram_bcm *bcm;
struct bhnd_nvram_bcm_hvar *hvar;
bcm = (struct bhnd_nvram_bcm *)nv;
/* Handle header variables */
if ((hvar = bhnd_nvram_bcm_to_hdrvar(bcm, cookiep)) != NULL) {
return (hvar->name);
}
/* Cookie points to key\0value\0 */
return (cookiep);
}
/**
* Return the internal BCM data reference for a header-defined variable
* with @p name, or NULL if none exists.
*/
static struct bhnd_nvram_bcm_hvar *
bhnd_nvram_bcm_gethdrvar(struct bhnd_nvram_bcm *bcm, const char *name)
{
for (size_t i = 0; i < nitems(bcm->hvars); i++) {
if (strcmp(bcm->hvars[i].name, name) == 0)
return (&bcm->hvars[i]);
}
/* Not found */
return (NULL);
}
/**
* If @p cookiep references a header-defined variable, return the
* internal BCM data reference. Otherwise, returns NULL.
*/
static struct bhnd_nvram_bcm_hvar *
bhnd_nvram_bcm_to_hdrvar(struct bhnd_nvram_bcm *bcm, void *cookiep)
{
#ifdef BHND_NVRAM_INVARIANTS
uintptr_t base, ptr;
#endif
/* If the cookie falls within the hvar array, it's a
* header variable cookie */
if (nitems(bcm->hvars) == 0)
return (NULL);
if (cookiep < (void *)&bcm->hvars[0])
return (NULL);
if (cookiep > (void *)&bcm->hvars[nitems(bcm->hvars)-1])
return (NULL);
#ifdef BHND_NVRAM_INVARIANTS
base = (uintptr_t)bcm->hvars;
ptr = (uintptr_t)cookiep;
BHND_NV_ASSERT((ptr - base) % sizeof(bcm->hvars[0]) == 0,
("misaligned hvar pointer %p/%p", cookiep, bcm->hvars));
#endif /* INVARIANTS */
return ((struct bhnd_nvram_bcm_hvar *)cookiep);
}
/**
* Return the index of @p hdrvar within @p bcm's backing hvars array.
*/
static size_t
bhnd_nvram_bcm_hdrvar_index(struct bhnd_nvram_bcm *bcm,
struct bhnd_nvram_bcm_hvar *hdrvar)
{
BHND_NV_ASSERT(bhnd_nvram_bcm_to_hdrvar(bcm, (void *)hdrvar) != NULL,
("%p is not a valid hdrvar reference", hdrvar));
return (hdrvar - &bcm->hvars[0]);
}