1/*	$NetBSD: lwconfig.c,v 1.3.4.1 2012/06/05 21:14:54 bouyer Exp $	*/
2
3/*
4 * Copyright (C) 2004-2008, 2011, 2012  Internet Systems Consortium, Inc. ("ISC")
5 * Copyright (C) 2000-2003  Internet Software Consortium.
6 *
7 * Permission to use, copy, modify, and/or distribute this software for any
8 * purpose with or without fee is hereby granted, provided that the above
9 * copyright notice and this permission notice appear in all copies.
10 *
11 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
12 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
13 * AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
14 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
16 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17 * PERFORMANCE OF THIS SOFTWARE.
18 */
19
20/* Id */
21
22/*! \file */
23
24/**
25 * Module for parsing resolv.conf files.
26 *
27 *    lwres_conf_init() creates an empty lwres_conf_t structure for
28 *    lightweight resolver context ctx.
29 *
30 *    lwres_conf_clear() frees up all the internal memory used by that
31 *    lwres_conf_t structure in resolver context ctx.
32 *
33 *    lwres_conf_parse() opens the file filename and parses it to initialise
34 *    the resolver context ctx's lwres_conf_t structure.
35 *
36 *    lwres_conf_print() prints the lwres_conf_t structure for resolver
37 *    context ctx to the FILE fp.
38 *
39 * \section lwconfig_return Return Values
40 *
41 *    lwres_conf_parse() returns #LWRES_R_SUCCESS if it successfully read and
42 *    parsed filename. It returns #LWRES_R_FAILURE if filename could not be
43 *    opened or contained incorrect resolver statements.
44 *
45 *    lwres_conf_print() returns #LWRES_R_SUCCESS unless an error occurred
46 *    when converting the network addresses to a numeric host address
47 *    string. If this happens, the function returns #LWRES_R_FAILURE.
48 *
49 * \section lwconfig_see See Also
50 *
51 *    stdio(3), \link resolver resolver \endlink
52 *
53 * \section files Files
54 *
55 *    /etc/resolv.conf
56 */
57
58#include <config.h>
59
60#include <assert.h>
61#include <ctype.h>
62#include <errno.h>
63#include <stdlib.h>
64#include <stdio.h>
65#include <string.h>
66#include <unistd.h>
67
68#include <lwres/lwbuffer.h>
69#include <lwres/lwres.h>
70#include <lwres/net.h>
71#include <lwres/result.h>
72
73#include "assert_p.h"
74#include "context_p.h"
75
76
77#if ! defined(NS_INADDRSZ)
78#define NS_INADDRSZ	 4
79#endif
80
81#if ! defined(NS_IN6ADDRSZ)
82#define NS_IN6ADDRSZ	16
83#endif
84
85static lwres_result_t
86lwres_conf_parsenameserver(lwres_context_t *ctx,  FILE *fp);
87
88static lwres_result_t
89lwres_conf_parselwserver(lwres_context_t *ctx,  FILE *fp);
90
91static lwres_result_t
92lwres_conf_parsedomain(lwres_context_t *ctx, FILE *fp);
93
94static lwres_result_t
95lwres_conf_parsesearch(lwres_context_t *ctx,  FILE *fp);
96
97static lwres_result_t
98lwres_conf_parsesortlist(lwres_context_t *ctx,  FILE *fp);
99
100static lwres_result_t
101lwres_conf_parseoption(lwres_context_t *ctx,  FILE *fp);
102
103static void
104lwres_resetaddr(lwres_addr_t *addr);
105
106static lwres_result_t
107lwres_create_addr(const char *buff, lwres_addr_t *addr, int convert_zero);
108
109static int lwresaddr2af(int lwresaddrtype);
110
111
112static int
113lwresaddr2af(int lwresaddrtype)
114{
115	int af = 0;
116
117	switch (lwresaddrtype) {
118	case LWRES_ADDRTYPE_V4:
119		af = AF_INET;
120		break;
121
122	case LWRES_ADDRTYPE_V6:
123		af = AF_INET6;
124		break;
125	}
126
127	return (af);
128}
129
130
131/*!
132 * Eat characters from FP until EOL or EOF. Returns EOF or '\n'
133 */
134static int
135eatline(FILE *fp) {
136	int ch;
137
138	ch = fgetc(fp);
139	while (ch != '\n' && ch != EOF)
140		ch = fgetc(fp);
141
142	return (ch);
143}
144
145
146/*!
147 * Eats white space up to next newline or non-whitespace character (of
148 * EOF). Returns the last character read. Comments are considered white
149 * space.
150 */
151static int
152eatwhite(FILE *fp) {
153	int ch;
154
155	ch = fgetc(fp);
156	while (ch != '\n' && ch != EOF && isspace((unsigned char)ch))
157		ch = fgetc(fp);
158
159	if (ch == ';' || ch == '#')
160		ch = eatline(fp);
161
162	return (ch);
163}
164
165
166/*!
167 * Skip over any leading whitespace and then read in the next sequence of
168 * non-whitespace characters. In this context newline is not considered
169 * whitespace. Returns EOF on end-of-file, or the character
170 * that caused the reading to stop.
171 */
172static int
173getword(FILE *fp, char *buffer, size_t size) {
174	int ch;
175	char *p = buffer;
176
177	REQUIRE(buffer != NULL);
178	REQUIRE(size > 0U);
179
180	*p = '\0';
181
182	ch = eatwhite(fp);
183
184	if (ch == EOF)
185		return (EOF);
186
187	for (;;) {
188		*p = '\0';
189
190		if (ch == EOF || isspace((unsigned char)ch))
191			break;
192		else if ((size_t) (p - buffer) == size - 1)
193			return (EOF);	/* Not enough space. */
194
195		*p++ = (char)ch;
196		ch = fgetc(fp);
197	}
198
199	return (ch);
200}
201
202static void
203lwres_resetaddr(lwres_addr_t *addr) {
204	REQUIRE(addr != NULL);
205
206	memset(addr->address, 0, LWRES_ADDR_MAXLEN);
207	addr->family = 0;
208	addr->length = 0;
209}
210
211static char *
212lwres_strdup(lwres_context_t *ctx, const char *str) {
213	char *p;
214
215	REQUIRE(str != NULL);
216	REQUIRE(strlen(str) > 0U);
217
218	p = CTXMALLOC(strlen(str) + 1);
219	if (p != NULL)
220		strcpy(p, str);
221
222	return (p);
223}
224
225/*% intializes data structure for subsequent config parsing. */
226void
227lwres_conf_init(lwres_context_t *ctx) {
228	int i;
229	lwres_conf_t *confdata;
230
231	REQUIRE(ctx != NULL);
232	confdata = &ctx->confdata;
233
234	confdata->nsnext = 0;
235	confdata->lwnext = 0;
236	confdata->domainname = NULL;
237	confdata->searchnxt = 0;
238	confdata->sortlistnxt = 0;
239	confdata->resdebug = 0;
240	confdata->ndots = 1;
241	confdata->no_tld_query = 0;
242
243	for (i = 0; i < LWRES_CONFMAXNAMESERVERS; i++)
244		lwres_resetaddr(&confdata->nameservers[i]);
245
246	for (i = 0; i < LWRES_CONFMAXSEARCH; i++)
247		confdata->search[i] = NULL;
248
249	for (i = 0; i < LWRES_CONFMAXSORTLIST; i++) {
250		lwres_resetaddr(&confdata->sortlist[i].addr);
251		lwres_resetaddr(&confdata->sortlist[i].mask);
252	}
253}
254
255/*% Frees up all the internal memory used by the config data structure, returning it to the lwres_context_t. */
256void
257lwres_conf_clear(lwres_context_t *ctx) {
258	int i;
259	lwres_conf_t *confdata;
260
261	REQUIRE(ctx != NULL);
262	confdata = &ctx->confdata;
263
264	for (i = 0; i < confdata->nsnext; i++)
265		lwres_resetaddr(&confdata->nameservers[i]);
266
267	if (confdata->domainname != NULL) {
268		CTXFREE(confdata->domainname,
269			strlen(confdata->domainname) + 1);
270		confdata->domainname = NULL;
271	}
272
273	for (i = 0; i < confdata->searchnxt; i++) {
274		if (confdata->search[i] != NULL) {
275			CTXFREE(confdata->search[i],
276				strlen(confdata->search[i]) + 1);
277			confdata->search[i] = NULL;
278		}
279	}
280
281	for (i = 0; i < LWRES_CONFMAXSORTLIST; i++) {
282		lwres_resetaddr(&confdata->sortlist[i].addr);
283		lwres_resetaddr(&confdata->sortlist[i].mask);
284	}
285
286	confdata->nsnext = 0;
287	confdata->lwnext = 0;
288	confdata->domainname = NULL;
289	confdata->searchnxt = 0;
290	confdata->sortlistnxt = 0;
291	confdata->resdebug = 0;
292	confdata->ndots = 1;
293	confdata->no_tld_query = 0;
294}
295
296static lwres_result_t
297lwres_conf_parsenameserver(lwres_context_t *ctx,  FILE *fp) {
298	char word[LWRES_CONFMAXLINELEN];
299	int res;
300	lwres_conf_t *confdata;
301	lwres_addr_t address;
302
303	confdata = &ctx->confdata;
304
305	if (confdata->nsnext == LWRES_CONFMAXNAMESERVERS)
306		return (LWRES_R_SUCCESS);
307
308	res = getword(fp, word, sizeof(word));
309	if (strlen(word) == 0U)
310		return (LWRES_R_FAILURE); /* Nothing on line. */
311	else if (res == ' ' || res == '\t')
312		res = eatwhite(fp);
313
314	if (res != EOF && res != '\n')
315		return (LWRES_R_FAILURE); /* Extra junk on line. */
316
317	res = lwres_create_addr(word, &address, 1);
318	if (res == LWRES_R_SUCCESS &&
319	    ((address.family == LWRES_ADDRTYPE_V4 && ctx->use_ipv4 == 1) ||
320	     (address.family == LWRES_ADDRTYPE_V6 && ctx->use_ipv6 == 1))) {
321		confdata->nameservers[confdata->nsnext++] = address;
322	}
323
324	return (LWRES_R_SUCCESS);
325}
326
327static lwres_result_t
328lwres_conf_parselwserver(lwres_context_t *ctx,  FILE *fp) {
329	char word[LWRES_CONFMAXLINELEN];
330	int res;
331	lwres_conf_t *confdata;
332
333	confdata = &ctx->confdata;
334
335	if (confdata->lwnext == LWRES_CONFMAXLWSERVERS)
336		return (LWRES_R_SUCCESS);
337
338	res = getword(fp, word, sizeof(word));
339	if (strlen(word) == 0U)
340		return (LWRES_R_FAILURE); /* Nothing on line. */
341	else if (res == ' ' || res == '\t')
342		res = eatwhite(fp);
343
344	if (res != EOF && res != '\n')
345		return (LWRES_R_FAILURE); /* Extra junk on line. */
346
347	res = lwres_create_addr(word,
348				&confdata->lwservers[confdata->lwnext++], 1);
349	if (res != LWRES_R_SUCCESS)
350		return (res);
351
352	return (LWRES_R_SUCCESS);
353}
354
355static lwres_result_t
356lwres_conf_parsedomain(lwres_context_t *ctx,  FILE *fp) {
357	char word[LWRES_CONFMAXLINELEN];
358	int res, i;
359	lwres_conf_t *confdata;
360
361	confdata = &ctx->confdata;
362
363	res = getword(fp, word, sizeof(word));
364	if (strlen(word) == 0U)
365		return (LWRES_R_FAILURE); /* Nothing else on line. */
366	else if (res == ' ' || res == '\t')
367		res = eatwhite(fp);
368
369	if (res != EOF && res != '\n')
370		return (LWRES_R_FAILURE); /* Extra junk on line. */
371
372	if (confdata->domainname != NULL)
373		CTXFREE(confdata->domainname,
374			strlen(confdata->domainname) + 1); /*  */
375
376	/*
377	 * Search and domain are mutually exclusive.
378	 */
379	for (i = 0; i < LWRES_CONFMAXSEARCH; i++) {
380		if (confdata->search[i] != NULL) {
381			CTXFREE(confdata->search[i],
382				strlen(confdata->search[i])+1);
383			confdata->search[i] = NULL;
384		}
385	}
386	confdata->searchnxt = 0;
387
388	confdata->domainname = lwres_strdup(ctx, word);
389
390	if (confdata->domainname == NULL)
391		return (LWRES_R_FAILURE);
392
393	return (LWRES_R_SUCCESS);
394}
395
396static lwres_result_t
397lwres_conf_parsesearch(lwres_context_t *ctx,  FILE *fp) {
398	int idx, delim;
399	char word[LWRES_CONFMAXLINELEN];
400	lwres_conf_t *confdata;
401
402	confdata = &ctx->confdata;
403
404	if (confdata->domainname != NULL) {
405		/*
406		 * Search and domain are mutually exclusive.
407		 */
408		CTXFREE(confdata->domainname,
409			strlen(confdata->domainname) + 1);
410		confdata->domainname = NULL;
411	}
412
413	/*
414	 * Remove any previous search definitions.
415	 */
416	for (idx = 0; idx < LWRES_CONFMAXSEARCH; idx++) {
417		if (confdata->search[idx] != NULL) {
418			CTXFREE(confdata->search[idx],
419				strlen(confdata->search[idx])+1);
420			confdata->search[idx] = NULL;
421		}
422	}
423	confdata->searchnxt = 0;
424
425	delim = getword(fp, word, sizeof(word));
426	if (strlen(word) == 0U)
427		return (LWRES_R_FAILURE); /* Nothing else on line. */
428
429	idx = 0;
430	while (strlen(word) > 0U) {
431		if (confdata->searchnxt == LWRES_CONFMAXSEARCH)
432			goto ignore; /* Too many domains. */
433
434		confdata->search[idx] = lwres_strdup(ctx, word);
435		if (confdata->search[idx] == NULL)
436			return (LWRES_R_FAILURE);
437		idx++;
438		confdata->searchnxt++;
439
440	ignore:
441		if (delim == EOF || delim == '\n')
442			break;
443		else
444			delim = getword(fp, word, sizeof(word));
445	}
446
447	return (LWRES_R_SUCCESS);
448}
449
450static lwres_result_t
451lwres_create_addr(const char *buffer, lwres_addr_t *addr, int convert_zero) {
452	struct in_addr v4;
453	struct in6_addr v6;
454
455	if (lwres_net_aton(buffer, &v4) == 1) {
456		if (convert_zero) {
457			unsigned char zeroaddress[] = {0, 0, 0, 0};
458			unsigned char loopaddress[] = {127, 0, 0, 1};
459			if (memcmp(&v4, zeroaddress, 4) == 0)
460				memcpy(&v4, loopaddress, 4);
461		}
462		addr->family = LWRES_ADDRTYPE_V4;
463		addr->length = NS_INADDRSZ;
464		memcpy((void *)addr->address, &v4, NS_INADDRSZ);
465
466	} else if (lwres_net_pton(AF_INET6, buffer, &v6) == 1) {
467		addr->family = LWRES_ADDRTYPE_V6;
468		addr->length = NS_IN6ADDRSZ;
469		memcpy((void *)addr->address, &v6, NS_IN6ADDRSZ);
470	} else {
471		return (LWRES_R_FAILURE); /* Unrecognised format. */
472	}
473
474	return (LWRES_R_SUCCESS);
475}
476
477static lwres_result_t
478lwres_conf_parsesortlist(lwres_context_t *ctx,  FILE *fp) {
479	int delim, res, idx;
480	char word[LWRES_CONFMAXLINELEN];
481	char *p;
482	lwres_conf_t *confdata;
483
484	confdata = &ctx->confdata;
485
486	delim = getword(fp, word, sizeof(word));
487	if (strlen(word) == 0U)
488		return (LWRES_R_FAILURE); /* Empty line after keyword. */
489
490	while (strlen(word) > 0U) {
491		if (confdata->sortlistnxt == LWRES_CONFMAXSORTLIST)
492			return (LWRES_R_FAILURE); /* Too many values. */
493
494		p = strchr(word, '/');
495		if (p != NULL)
496			*p++ = '\0';
497
498		idx = confdata->sortlistnxt;
499		res = lwres_create_addr(word, &confdata->sortlist[idx].addr, 1);
500		if (res != LWRES_R_SUCCESS)
501			return (res);
502
503		if (p != NULL) {
504			res = lwres_create_addr(p,
505						&confdata->sortlist[idx].mask,
506						0);
507			if (res != LWRES_R_SUCCESS)
508				return (res);
509		} else {
510			/*
511			 * Make up a mask.
512			 */
513			confdata->sortlist[idx].mask =
514				confdata->sortlist[idx].addr;
515
516			memset(&confdata->sortlist[idx].mask.address, 0xff,
517			       confdata->sortlist[idx].addr.length);
518		}
519
520		confdata->sortlistnxt++;
521
522		if (delim == EOF || delim == '\n')
523			break;
524		else
525			delim = getword(fp, word, sizeof(word));
526	}
527
528	return (LWRES_R_SUCCESS);
529}
530
531static lwres_result_t
532lwres_conf_parseoption(lwres_context_t *ctx,  FILE *fp) {
533	int delim;
534	long ndots;
535	char *p;
536	char word[LWRES_CONFMAXLINELEN];
537	lwres_conf_t *confdata;
538
539	REQUIRE(ctx != NULL);
540	confdata = &ctx->confdata;
541
542	delim = getword(fp, word, sizeof(word));
543	if (strlen(word) == 0U)
544		return (LWRES_R_FAILURE); /* Empty line after keyword. */
545
546	while (strlen(word) > 0U) {
547		if (strcmp("debug", word) == 0) {
548			confdata->resdebug = 1;
549		} else if (strcmp("no_tld_query", word) == 0) {
550			confdata->no_tld_query = 1;
551		} else if (strncmp("ndots:", word, 6) == 0) {
552			ndots = strtol(word + 6, &p, 10);
553			if (*p != '\0') /* Bad string. */
554				return (LWRES_R_FAILURE);
555			if (ndots < 0 || ndots > 0xff) /* Out of range. */
556				return (LWRES_R_FAILURE);
557			confdata->ndots = (lwres_uint8_t)ndots;
558		}
559
560		if (delim == EOF || delim == '\n')
561			break;
562		else
563			delim = getword(fp, word, sizeof(word));
564	}
565
566	return (LWRES_R_SUCCESS);
567}
568
569/*% parses a file and fills in the data structure. */
570lwres_result_t
571lwres_conf_parse(lwres_context_t *ctx, const char *filename) {
572	FILE *fp = NULL;
573	char word[256];
574	lwres_result_t rval, ret;
575	lwres_conf_t *confdata;
576	int stopchar;
577
578	REQUIRE(ctx != NULL);
579	confdata = &ctx->confdata;
580
581	REQUIRE(filename != NULL);
582	REQUIRE(strlen(filename) > 0U);
583	REQUIRE(confdata != NULL);
584
585	errno = 0;
586	if ((fp = fopen(filename, "r")) == NULL)
587		return (LWRES_R_NOTFOUND);
588
589	ret = LWRES_R_SUCCESS;
590	for (;;) {
591		stopchar = getword(fp, word, sizeof(word));
592		if (stopchar == EOF) {
593			rval = LWRES_R_SUCCESS;
594			POST(rval);
595			break;
596		}
597
598		if (strlen(word) == 0U)
599			rval = LWRES_R_SUCCESS;
600		else if (strcmp(word, "nameserver") == 0)
601			rval = lwres_conf_parsenameserver(ctx, fp);
602		else if (strcmp(word, "lwserver") == 0)
603			rval = lwres_conf_parselwserver(ctx, fp);
604		else if (strcmp(word, "domain") == 0)
605			rval = lwres_conf_parsedomain(ctx, fp);
606		else if (strcmp(word, "search") == 0)
607			rval = lwres_conf_parsesearch(ctx, fp);
608		else if (strcmp(word, "sortlist") == 0)
609			rval = lwres_conf_parsesortlist(ctx, fp);
610		else if (strcmp(word, "options") == 0)
611			rval = lwres_conf_parseoption(ctx, fp);
612		else {
613			/* unrecognised word. Ignore entire line */
614			rval = LWRES_R_SUCCESS;
615			stopchar = eatline(fp);
616			if (stopchar == EOF) {
617				break;
618			}
619		}
620		if (ret == LWRES_R_SUCCESS && rval != LWRES_R_SUCCESS)
621			ret = rval;
622	}
623
624	fclose(fp);
625
626	return (ret);
627}
628
629/*% Prints the config data structure to the FILE. */
630lwres_result_t
631lwres_conf_print(lwres_context_t *ctx, FILE *fp) {
632	int i;
633	int af;
634	char tmp[sizeof("ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255")];
635	const char *p;
636	lwres_conf_t *confdata;
637	lwres_addr_t tmpaddr;
638
639	REQUIRE(ctx != NULL);
640	confdata = &ctx->confdata;
641
642	REQUIRE(confdata->nsnext <= LWRES_CONFMAXNAMESERVERS);
643
644	for (i = 0; i < confdata->nsnext; i++) {
645		af = lwresaddr2af(confdata->nameservers[i].family);
646
647		p = lwres_net_ntop(af, confdata->nameservers[i].address,
648				   tmp, sizeof(tmp));
649		if (p != tmp)
650			return (LWRES_R_FAILURE);
651
652		fprintf(fp, "nameserver %s\n", tmp);
653	}
654
655	for (i = 0; i < confdata->lwnext; i++) {
656		af = lwresaddr2af(confdata->lwservers[i].family);
657
658		p = lwres_net_ntop(af, confdata->lwservers[i].address,
659				   tmp, sizeof(tmp));
660		if (p != tmp)
661			return (LWRES_R_FAILURE);
662
663		fprintf(fp, "lwserver %s\n", tmp);
664	}
665
666	if (confdata->domainname != NULL) {
667		fprintf(fp, "domain %s\n", confdata->domainname);
668	} else if (confdata->searchnxt > 0) {
669		REQUIRE(confdata->searchnxt <= LWRES_CONFMAXSEARCH);
670
671		fprintf(fp, "search");
672		for (i = 0; i < confdata->searchnxt; i++)
673			fprintf(fp, " %s", confdata->search[i]);
674		fputc('\n', fp);
675	}
676
677	REQUIRE(confdata->sortlistnxt <= LWRES_CONFMAXSORTLIST);
678
679	if (confdata->sortlistnxt > 0) {
680		fputs("sortlist", fp);
681		for (i = 0; i < confdata->sortlistnxt; i++) {
682			af = lwresaddr2af(confdata->sortlist[i].addr.family);
683
684			p = lwres_net_ntop(af,
685					   confdata->sortlist[i].addr.address,
686					   tmp, sizeof(tmp));
687			if (p != tmp)
688				return (LWRES_R_FAILURE);
689
690			fprintf(fp, " %s", tmp);
691
692			tmpaddr = confdata->sortlist[i].mask;
693			memset(&tmpaddr.address, 0xff, tmpaddr.length);
694
695			if (memcmp(&tmpaddr.address,
696				   confdata->sortlist[i].mask.address,
697				   confdata->sortlist[i].mask.length) != 0) {
698				af = lwresaddr2af(
699					    confdata->sortlist[i].mask.family);
700				p = lwres_net_ntop
701					(af,
702					 confdata->sortlist[i].mask.address,
703					 tmp, sizeof(tmp));
704				if (p != tmp)
705					return (LWRES_R_FAILURE);
706
707				fprintf(fp, "/%s", tmp);
708			}
709		}
710		fputc('\n', fp);
711	}
712
713	if (confdata->resdebug)
714		fprintf(fp, "options debug\n");
715
716	if (confdata->ndots > 0)
717		fprintf(fp, "options ndots:%d\n", confdata->ndots);
718
719	if (confdata->no_tld_query)
720		fprintf(fp, "options no_tld_query\n");
721
722	return (LWRES_R_SUCCESS);
723}
724
725/*% Returns a pointer to the current config structure. */
726lwres_conf_t *
727lwres_conf_get(lwres_context_t *ctx) {
728	REQUIRE(ctx != NULL);
729
730	return (&ctx->confdata);
731}
732