Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
617 lines (574 sloc)
19.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "etherboot.h" | |
#include "init.h" | |
#include "proto.h" | |
#include <gpxe/in.h> | |
#include "nic.h" | |
/* NOTE: the NFS code is heavily inspired by the NetBSD netboot code (read: | |
* large portions are copied verbatim) as distributed in OSKit 0.97. A few | |
* changes were necessary to adapt the code to Etherboot and to fix several | |
* inconsistencies. Also the RPC message preparation is done "by hand" to | |
* avoid adding netsprintf() which I find hard to understand and use. */ | |
/* NOTE 2: Etherboot does not care about things beyond the kernel image, so | |
* it loads the kernel image off the boot server (ARP_SERVER) and does not | |
* access the client root disk (root-path in dhcpd.conf), which would use | |
* ARP_ROOTSERVER. The root disk is something the operating system we are | |
* about to load needs to use. This is different from the OSKit 0.97 logic. */ | |
/* NOTE 3: Symlink handling introduced by Anselm M Hoffmeister, 2003-July-14 | |
* If a symlink is encountered, it is followed as far as possible (recursion | |
* possible, maximum 16 steps). There is no clearing of ".."'s inside the | |
* path, so please DON'T DO THAT. thx. */ | |
#define START_OPORT 700 /* mountd usually insists on secure ports */ | |
#define OPORT_SWEEP 200 /* make sure we don't leave secure range */ | |
static int oport = START_OPORT; | |
static struct sockaddr_in mount_server; | |
static struct sockaddr_in nfs_server; | |
static unsigned long rpc_id; | |
/************************************************************************** | |
RPC_INIT - set up the ID counter to something fairly random | |
**************************************************************************/ | |
static void rpc_init(void) | |
{ | |
unsigned long t; | |
t = currticks(); | |
rpc_id = t ^ (t << 8) ^ (t << 16); | |
} | |
/************************************************************************** | |
RPC_PRINTERROR - Print a low level RPC error message | |
**************************************************************************/ | |
static void rpc_printerror(struct rpc_t *rpc) | |
{ | |
if (rpc->u.reply.rstatus || rpc->u.reply.verifier || | |
rpc->u.reply.astatus) { | |
/* rpc_printerror() is called for any RPC related error, | |
* suppress output if no low level RPC error happened. */ | |
DBG("RPC error: (%d,%d,%d)\n", ntohl(rpc->u.reply.rstatus), | |
ntohl(rpc->u.reply.verifier), | |
ntohl(rpc->u.reply.astatus)); | |
} | |
} | |
/************************************************************************** | |
AWAIT_RPC - Wait for an rpc packet | |
**************************************************************************/ | |
static int await_rpc(int ival, void *ptr, | |
unsigned short ptype __unused, struct iphdr *ip, | |
struct udphdr *udp, struct tcphdr *tcp __unused) | |
{ | |
struct rpc_t *rpc; | |
if (!udp) | |
return 0; | |
if (arptable[ARP_CLIENT].ipaddr.s_addr != ip->dest.s_addr) | |
return 0; | |
if (ntohs(udp->dest) != ival) | |
return 0; | |
if (nic.packetlen < ETH_HLEN + sizeof(struct iphdr) + sizeof(struct udphdr) + 8) | |
return 0; | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
if (*(unsigned long *)ptr != ntohl(rpc->u.reply.id)) | |
return 0; | |
if (MSG_REPLY != ntohl(rpc->u.reply.type)) | |
return 0; | |
return 1; | |
} | |
/************************************************************************** | |
RPC_LOOKUP - Lookup RPC Port numbers | |
**************************************************************************/ | |
static int rpc_lookup(struct sockaddr_in *addr, int prog, int ver, int sport) | |
{ | |
struct rpc_t buf, *rpc; | |
unsigned long id; | |
int retries; | |
long *p; | |
id = rpc_id++; | |
buf.u.call.id = htonl(id); | |
buf.u.call.type = htonl(MSG_CALL); | |
buf.u.call.rpcvers = htonl(2); /* use RPC version 2 */ | |
buf.u.call.prog = htonl(PROG_PORTMAP); | |
buf.u.call.vers = htonl(2); /* portmapper is version 2 */ | |
buf.u.call.proc = htonl(PORTMAP_GETPORT); | |
p = (long *)buf.u.call.data; | |
*p++ = 0; *p++ = 0; /* auth credential */ | |
*p++ = 0; *p++ = 0; /* auth verifier */ | |
*p++ = htonl(prog); | |
*p++ = htonl(ver); | |
*p++ = htonl(IP_UDP); | |
*p++ = 0; | |
for (retries = 0; retries < MAX_RPC_RETRIES; retries++) { | |
long timeout; | |
udp_transmit(addr->sin_addr.s_addr, sport, addr->sin_port, | |
(char *)p - (char *)&buf, &buf); | |
timeout = rfc2131_sleep_interval(TIMEOUT, retries); | |
if (await_reply(await_rpc, sport, &id, timeout)) { | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
if (rpc->u.reply.rstatus || rpc->u.reply.verifier || | |
rpc->u.reply.astatus) { | |
rpc_printerror(rpc); | |
return 0; | |
} else { | |
return ntohl(rpc->u.reply.data[0]); | |
} | |
} | |
} | |
return 0; | |
} | |
/************************************************************************** | |
RPC_ADD_CREDENTIALS - Add RPC authentication/verifier entries | |
**************************************************************************/ | |
static long *rpc_add_credentials(long *p) | |
{ | |
int hl; | |
/* Here's the executive summary on authentication requirements of the | |
* various NFS server implementations: Linux accepts both AUTH_NONE | |
* and AUTH_UNIX authentication (also accepts an empty hostname field | |
* in the AUTH_UNIX scheme). *BSD refuses AUTH_NONE, but accepts | |
* AUTH_UNIX (also accepts an empty hostname field in the AUTH_UNIX | |
* scheme). To be safe, use AUTH_UNIX and pass the hostname if we have | |
* it (if the BOOTP/DHCP reply didn't give one, just use an empty | |
* hostname). */ | |
hl = (hostnamelen + 3) & ~3; | |
/* Provide an AUTH_UNIX credential. */ | |
*p++ = htonl(1); /* AUTH_UNIX */ | |
*p++ = htonl(hl+20); /* auth length */ | |
*p++ = htonl(0); /* stamp */ | |
*p++ = htonl(hostnamelen); /* hostname string */ | |
if (hostnamelen & 3) { | |
*(p + hostnamelen / 4) = 0; /* add zero padding */ | |
} | |
memcpy(p, hostname, hostnamelen); | |
p += hl / 4; | |
*p++ = 0; /* uid */ | |
*p++ = 0; /* gid */ | |
*p++ = 0; /* auxiliary gid list */ | |
/* Provide an AUTH_NONE verifier. */ | |
*p++ = 0; /* AUTH_NONE */ | |
*p++ = 0; /* auth length */ | |
return p; | |
} | |
/************************************************************************** | |
NFS_PRINTERROR - Print a NFS error message | |
**************************************************************************/ | |
static void nfs_printerror(int err) | |
{ | |
switch (-err) { | |
case NFSERR_PERM: | |
printf("Not owner\n"); | |
break; | |
case NFSERR_NOENT: | |
printf("No such file or directory\n"); | |
break; | |
case NFSERR_ACCES: | |
printf("Permission denied\n"); | |
break; | |
case NFSERR_ISDIR: | |
printf("Directory given where filename expected\n"); | |
break; | |
case NFSERR_INVAL: | |
printf("Invalid filehandle\n"); | |
break; // INVAL is not defined in NFSv2, some NFS-servers | |
// seem to use it in answers to v2 nevertheless. | |
case 9998: | |
printf("low-level RPC failure (parameter decoding problem?)\n"); | |
break; | |
case 9999: | |
printf("low-level RPC failure (authentication problem?)\n"); | |
break; | |
default: | |
printf("Unknown NFS error %d\n", -err); | |
} | |
} | |
/************************************************************************** | |
NFS_MOUNT - Mount an NFS Filesystem | |
**************************************************************************/ | |
static int nfs_mount(struct sockaddr_in *server, char *path, char *fh, int sport) | |
{ | |
struct rpc_t buf, *rpc; | |
unsigned long id; | |
int retries; | |
long *p; | |
int pathlen = strlen(path); | |
id = rpc_id++; | |
buf.u.call.id = htonl(id); | |
buf.u.call.type = htonl(MSG_CALL); | |
buf.u.call.rpcvers = htonl(2); /* use RPC version 2 */ | |
buf.u.call.prog = htonl(PROG_MOUNT); | |
buf.u.call.vers = htonl(1); /* mountd is version 1 */ | |
buf.u.call.proc = htonl(MOUNT_ADDENTRY); | |
p = rpc_add_credentials((long *)buf.u.call.data); | |
*p++ = htonl(pathlen); | |
if (pathlen & 3) { | |
*(p + pathlen / 4) = 0; /* add zero padding */ | |
} | |
memcpy(p, path, pathlen); | |
p += (pathlen + 3) / 4; | |
for (retries = 0; retries < MAX_RPC_RETRIES; retries++) { | |
long timeout; | |
udp_transmit(server->sin_addr.s_addr, sport, server->sin_port, | |
(char *)p - (char *)&buf, &buf); | |
timeout = rfc2131_sleep_interval(TIMEOUT, retries); | |
if (await_reply(await_rpc, sport, &id, timeout)) { | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
if (rpc->u.reply.rstatus || rpc->u.reply.verifier || | |
rpc->u.reply.astatus || rpc->u.reply.data[0]) { | |
rpc_printerror(rpc); | |
if (rpc->u.reply.rstatus) { | |
/* RPC failed, no verifier, data[0] */ | |
return -9999; | |
} | |
if (rpc->u.reply.astatus) { | |
/* RPC couldn't decode parameters */ | |
return -9998; | |
} | |
return -ntohl(rpc->u.reply.data[0]); | |
} else { | |
memcpy(fh, rpc->u.reply.data + 1, NFS_FHSIZE); | |
return 0; | |
} | |
} | |
} | |
return -1; | |
} | |
/************************************************************************** | |
NFS_UMOUNTALL - Unmount all our NFS Filesystems on the Server | |
**************************************************************************/ | |
static void nfs_umountall(struct sockaddr_in *server) | |
{ | |
struct rpc_t buf, *rpc; | |
unsigned long id; | |
int retries; | |
long *p; | |
id = rpc_id++; | |
buf.u.call.id = htonl(id); | |
buf.u.call.type = htonl(MSG_CALL); | |
buf.u.call.rpcvers = htonl(2); /* use RPC version 2 */ | |
buf.u.call.prog = htonl(PROG_MOUNT); | |
buf.u.call.vers = htonl(1); /* mountd is version 1 */ | |
buf.u.call.proc = htonl(MOUNT_UMOUNTALL); | |
p = rpc_add_credentials((long *)buf.u.call.data); | |
for (retries = 0; retries < MAX_RPC_RETRIES; retries++) { | |
long timeout = rfc2131_sleep_interval(TIMEOUT, retries); | |
udp_transmit(server->sin_addr.s_addr, oport, server->sin_port, | |
(char *)p - (char *)&buf, &buf); | |
if (await_reply(await_rpc, oport, &id, timeout)) { | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
if (rpc->u.reply.rstatus || rpc->u.reply.verifier || | |
rpc->u.reply.astatus) { | |
rpc_printerror(rpc); | |
} | |
break; | |
} | |
} | |
} | |
/************************************************************************** | |
NFS_RESET - Reset the NFS subsystem | |
**************************************************************************/ | |
static void nfs_reset ( void ) { | |
/* If we have a mount server, call nfs_umountall() */ | |
if ( mount_server.sin_addr.s_addr ) { | |
nfs_umountall ( &mount_server ); | |
} | |
/* Zero the data structures */ | |
memset ( &mount_server, 0, sizeof ( mount_server ) ); | |
memset ( &nfs_server, 0, sizeof ( nfs_server ) ); | |
} | |
/*************************************************************************** | |
* NFS_READLINK (AH 2003-07-14) | |
* This procedure is called when read of the first block fails - | |
* this probably happens when it's a directory or a symlink | |
* In case of successful readlink(), the dirname is manipulated, | |
* so that inside the nfs() function a recursion can be done. | |
**************************************************************************/ | |
static int nfs_readlink(struct sockaddr_in *server, char *fh __unused, | |
char *path, char *nfh, int sport) | |
{ | |
struct rpc_t buf, *rpc; | |
unsigned long id; | |
long *p; | |
int retries; | |
int pathlen = strlen(path); | |
id = rpc_id++; | |
buf.u.call.id = htonl(id); | |
buf.u.call.type = htonl(MSG_CALL); | |
buf.u.call.rpcvers = htonl(2); /* use RPC version 2 */ | |
buf.u.call.prog = htonl(PROG_NFS); | |
buf.u.call.vers = htonl(2); /* nfsd is version 2 */ | |
buf.u.call.proc = htonl(NFS_READLINK); | |
p = rpc_add_credentials((long *)buf.u.call.data); | |
memcpy(p, nfh, NFS_FHSIZE); | |
p += (NFS_FHSIZE / 4); | |
for (retries = 0; retries < MAX_RPC_RETRIES; retries++) { | |
long timeout = rfc2131_sleep_interval(TIMEOUT, retries); | |
udp_transmit(server->sin_addr.s_addr, sport, server->sin_port, | |
(char *)p - (char *)&buf, &buf); | |
if (await_reply(await_rpc, sport, &id, timeout)) { | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
if (rpc->u.reply.rstatus || rpc->u.reply.verifier || | |
rpc->u.reply.astatus || rpc->u.reply.data[0]) { | |
rpc_printerror(rpc); | |
if (rpc->u.reply.rstatus) { | |
/* RPC failed, no verifier, data[0] */ | |
return -9999; | |
} | |
if (rpc->u.reply.astatus) { | |
/* RPC couldn't decode parameters */ | |
return -9998; | |
} | |
return -ntohl(rpc->u.reply.data[0]); | |
} else { | |
// It *is* a link. | |
// If it's a relative link, append everything to dirname, filename TOO! | |
retries = strlen ( (char *)(&(rpc->u.reply.data[2]) )); | |
if ( *((char *)(&(rpc->u.reply.data[2]))) != '/' ) { | |
path[pathlen++] = '/'; | |
while ( ( retries + pathlen ) > 298 ) { | |
retries--; | |
} | |
if ( retries > 0 ) { | |
memcpy(path + pathlen, &(rpc->u.reply.data[2]), retries + 1); | |
} else { retries = 0; } | |
path[pathlen + retries] = 0; | |
} else { | |
// Else make it the only path. | |
if ( retries > 298 ) { retries = 298; } | |
memcpy ( path, &(rpc->u.reply.data[2]), retries + 1 ); | |
path[retries] = 0; | |
} | |
return 0; | |
} | |
} | |
} | |
return -1; | |
} | |
/************************************************************************** | |
NFS_LOOKUP - Lookup Pathname | |
**************************************************************************/ | |
static int nfs_lookup(struct sockaddr_in *server, char *fh, char *path, char *nfh, | |
int sport) | |
{ | |
struct rpc_t buf, *rpc; | |
unsigned long id; | |
long *p; | |
int retries; | |
int pathlen = strlen(path); | |
id = rpc_id++; | |
buf.u.call.id = htonl(id); | |
buf.u.call.type = htonl(MSG_CALL); | |
buf.u.call.rpcvers = htonl(2); /* use RPC version 2 */ | |
buf.u.call.prog = htonl(PROG_NFS); | |
buf.u.call.vers = htonl(2); /* nfsd is version 2 */ | |
buf.u.call.proc = htonl(NFS_LOOKUP); | |
p = rpc_add_credentials((long *)buf.u.call.data); | |
memcpy(p, fh, NFS_FHSIZE); | |
p += (NFS_FHSIZE / 4); | |
*p++ = htonl(pathlen); | |
if (pathlen & 3) { | |
*(p + pathlen / 4) = 0; /* add zero padding */ | |
} | |
memcpy(p, path, pathlen); | |
p += (pathlen + 3) / 4; | |
for (retries = 0; retries < MAX_RPC_RETRIES; retries++) { | |
long timeout = rfc2131_sleep_interval(TIMEOUT, retries); | |
udp_transmit(server->sin_addr.s_addr, sport, server->sin_port, | |
(char *)p - (char *)&buf, &buf); | |
if (await_reply(await_rpc, sport, &id, timeout)) { | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
if (rpc->u.reply.rstatus || rpc->u.reply.verifier || | |
rpc->u.reply.astatus || rpc->u.reply.data[0]) { | |
rpc_printerror(rpc); | |
if (rpc->u.reply.rstatus) { | |
/* RPC failed, no verifier, data[0] */ | |
return -9999; | |
} | |
if (rpc->u.reply.astatus) { | |
/* RPC couldn't decode parameters */ | |
return -9998; | |
} | |
return -ntohl(rpc->u.reply.data[0]); | |
} else { | |
memcpy(nfh, rpc->u.reply.data + 1, NFS_FHSIZE); | |
return 0; | |
} | |
} | |
} | |
return -1; | |
} | |
/************************************************************************** | |
NFS_READ - Read File on NFS Server | |
**************************************************************************/ | |
static int nfs_read(struct sockaddr_in *server, char *fh, int offset, int len, | |
int sport) | |
{ | |
struct rpc_t buf, *rpc; | |
unsigned long id; | |
int retries; | |
long *p; | |
static int tokens=0; | |
/* | |
* Try to implement something similar to a window protocol in | |
* terms of response to losses. On successful receive, increment | |
* the number of tokens by 1 (cap at 256). On failure, halve it. | |
* When the number of tokens is >= 2, use a very short timeout. | |
*/ | |
id = rpc_id++; | |
buf.u.call.id = htonl(id); | |
buf.u.call.type = htonl(MSG_CALL); | |
buf.u.call.rpcvers = htonl(2); /* use RPC version 2 */ | |
buf.u.call.prog = htonl(PROG_NFS); | |
buf.u.call.vers = htonl(2); /* nfsd is version 2 */ | |
buf.u.call.proc = htonl(NFS_READ); | |
p = rpc_add_credentials((long *)buf.u.call.data); | |
memcpy(p, fh, NFS_FHSIZE); | |
p += NFS_FHSIZE / 4; | |
*p++ = htonl(offset); | |
*p++ = htonl(len); | |
*p++ = 0; /* unused parameter */ | |
for (retries = 0; retries < MAX_RPC_RETRIES; retries++) { | |
long timeout = rfc2131_sleep_interval(TIMEOUT, retries); | |
if (tokens >= 2) | |
timeout = TICKS_PER_SEC/2; | |
udp_transmit(server->sin_addr.s_addr, sport, server->sin_port, | |
(char *)p - (char *)&buf, &buf); | |
if (await_reply(await_rpc, sport, &id, timeout)) { | |
if (tokens < 256) | |
tokens++; | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
if (rpc->u.reply.rstatus || rpc->u.reply.verifier || | |
rpc->u.reply.astatus || rpc->u.reply.data[0]) { | |
rpc_printerror(rpc); | |
if (rpc->u.reply.rstatus) { | |
/* RPC failed, no verifier, data[0] */ | |
return -9999; | |
} | |
if (rpc->u.reply.astatus) { | |
/* RPC couldn't decode parameters */ | |
return -9998; | |
} | |
return -ntohl(rpc->u.reply.data[0]); | |
} else { | |
return 0; | |
} | |
} else | |
tokens >>= 1; | |
} | |
return -1; | |
} | |
/************************************************************************** | |
NFS - Download extended BOOTP data, or kernel image from NFS server | |
**************************************************************************/ | |
static int nfs ( char *url __unused, struct sockaddr_in *server, | |
char *name, struct buffer *buffer ) { | |
static int recursion = 0; | |
int sport; | |
int err, namelen = strlen(name); | |
char dirname[300], *fname; | |
char dirfh[NFS_FHSIZE]; /* file handle of directory */ | |
char filefh[NFS_FHSIZE]; /* file handle of kernel image */ | |
int rlen, size, offs, len; | |
struct rpc_t *rpc; | |
sport = oport++; | |
if (oport > START_OPORT+OPORT_SWEEP) { | |
oport = START_OPORT; | |
} | |
mount_server.sin_addr = nfs_server.sin_addr = server->sin_addr; | |
mount_server.sin_port = rpc_lookup(server, PROG_MOUNT, 1, sport); | |
if ( ! mount_server.sin_port ) { | |
DBG ( "Cannot get mount port from %!:%d\n", | |
server->sin_addr.s_addr, server->sin_port ); | |
return 0; | |
} | |
nfs_server.sin_port = rpc_lookup(server, PROG_NFS, 2, sport); | |
if ( ! mount_server.sin_port ) { | |
DBG ( "Cannot get nfs port from %!:%d\n", | |
server->sin_addr.s_addr, server->sin_port ); | |
return 0; | |
} | |
if ( name != dirname ) { | |
memcpy(dirname, name, namelen + 1); | |
} | |
recursion = 0; | |
nfssymlink: | |
if ( recursion > NFS_MAXLINKDEPTH ) { | |
DBG ( "\nRecursion: More than %d symlinks followed. Abort.\n", | |
NFS_MAXLINKDEPTH ); | |
return 0; | |
} | |
recursion++; | |
fname = dirname + (namelen - 1); | |
while (fname >= dirname) { | |
if (*fname == '/') { | |
*fname = '\0'; | |
fname++; | |
break; | |
} | |
fname--; | |
} | |
if (fname < dirname) { | |
DBG("can't parse file name %s\n", name); | |
return 0; | |
} | |
err = nfs_mount(&mount_server, dirname, dirfh, sport); | |
if (err) { | |
DBG("mounting %s: ", dirname); | |
nfs_printerror(err); | |
/* just to be sure... */ | |
nfs_reset(); | |
return 0; | |
} | |
err = nfs_lookup(&nfs_server, dirfh, fname, filefh, sport); | |
if (err) { | |
DBG("looking up %s: ", fname); | |
nfs_printerror(err); | |
nfs_reset(); | |
return 0; | |
} | |
offs = 0; | |
size = -1; /* will be set properly with the first reply */ | |
len = NFS_READ_SIZE; /* first request is always full size */ | |
do { | |
err = nfs_read(&nfs_server, filefh, offs, len, sport); | |
if ((err <= -NFSERR_ISDIR)&&(err >= -NFSERR_INVAL) && (offs == 0)) { | |
// An error occured. NFS servers tend to sending | |
// errors 21 / 22 when symlink instead of real file | |
// is requested. So check if it's a symlink! | |
if ( nfs_readlink(&nfs_server, dirfh, dirname, | |
filefh, sport) == 0 ) { | |
printf("\nLoading symlink:%s ..",dirname); | |
goto nfssymlink; | |
} | |
nfs_printerror(err); | |
nfs_reset(); | |
return 0; | |
} | |
if (err) { | |
printf("\nError reading at offset %d: ", offs); | |
nfs_printerror(err); | |
nfs_reset(); | |
return 0; | |
} | |
rpc = (struct rpc_t *)&nic.packet[ETH_HLEN]; | |
/* size must be found out early to allow EOF detection */ | |
if (size == -1) { | |
size = ntohl(rpc->u.reply.data[6]); | |
} | |
rlen = ntohl(rpc->u.reply.data[18]); | |
if (rlen > len) { | |
rlen = len; /* shouldn't happen... */ | |
} | |
if ( ! fill_buffer ( buffer, &rpc->u.reply.data[19], | |
offs, rlen ) ) { | |
nfs_reset(); | |
return 0; | |
} | |
offs += rlen; | |
/* last request is done with matching requested read size */ | |
if (size-offs < NFS_READ_SIZE) { | |
len = size-offs; | |
} | |
} while (len != 0); | |
/* len == 0 means that all the file has been read */ | |
return 1; | |
} | |
INIT_FN ( INIT_RPC, rpc_init, nfs_reset, nfs_reset ); | |
struct protocol nfs_protocol __protocol = { | |
.name = "nfs", | |
.default_port = SUNRPC_PORT, | |
.load = nfs, | |
}; |