1 /* Copyright 2018, UCAR/Unidata and OPeNDAP, Inc.
2    See the COPYRIGHT file for more information. */
3 
4 #include "config.h"
5 #include <errno.h>
6 #ifdef HAVE_UNISTD_H
7 #include <unistd.h>
8 #endif
9 #ifdef HAVE_SYS_STAT_H
10 #include <sys/stat.h>
11 #endif
12 #ifdef HAVE_FCNTL_H
13 #include <fcntl.h>
14 #endif
15 
16 #ifdef _MSC_VER
17 #include <io.h>
18 #define mode_t int
19 #endif
20 
21 #include "ocinternal.h"
22 #include "ocdebug.h"
23 
24 /* Order is important: longest first */
25 static const char* DDSdatamarks[3] = {"Data:\r\n","Data:\n",(char*)NULL};
26 
27 /* Not all systems have strndup, so provide one*/
28 char*
ocstrndup(const char * s,size_t len)29 ocstrndup(const char* s, size_t len)
30 {
31     char* dup;
32     if(s == NULL) return NULL;
33     dup = (char*)ocmalloc(len+1);
34     MEMCHECK(dup,NULL);
35     memcpy((void*)dup,s,len);
36     dup[len] = '\0';
37     return dup;
38 }
39 
40 /* Do not trust strncmp semantics; this one
41    compares up to len chars or to null terminators */
42 int
ocstrncmp(const char * s1,const char * s2,size_t len)43 ocstrncmp(const char* s1, const char* s2, size_t len)
44 {
45     const char *p,*q;
46     if(s1 == s2) return 0;
47     if(s1 == NULL) return -1;
48     if(s2 == NULL) return +1;
49     for(p=s1,q=s2;len > 0;p++,q++,len--) {
50 	if(*p == 0 && *q == 0) return 0; /* *p == *q == 0 */
51 	if(*p != *q)
52 	    return (*p - *q);
53     }
54     /* 1st len chars are same */
55     return 0;
56 }
57 
58 
59 #if 0
60 void
61 makedimlist(Nclist* path, Nclist* dims)
62 {
63     unsigned int i,j;
64     for(i=0;i<nclistlength(path);i++) {
65 	OCnode* node = (OCnode*)nclistget(path,i);
66         unsigned int rank = node->array.rank;
67 	for(j=0;j<rank;j++) {
68 	    OCnode* dim = (OCnode*)nclistget(node->array.dimensions,j);
69 	    nclistpush(dims,(void*)dim);
70         }
71     }
72 }
73 #endif
74 
75 void
ocfreeprojectionclause(OCprojectionclause * clause)76 ocfreeprojectionclause(OCprojectionclause* clause)
77 {
78     if(clause->target != NULL) free(clause->target);
79     while(nclistlength(clause->indexsets) > 0) {
80 	NClist* slices = (NClist*)nclistpop(clause->indexsets);
81         while(nclistlength(slices) > 0) {
82 	    OCslice* slice = (OCslice*)nclistpop(slices);
83 	    if(slice != NULL) free(slice);
84 	}
85         nclistfree(slices);
86     }
87     nclistfree(clause->indexsets);
88     free(clause);
89 }
90 
91 #if 0
92 void
93 freeAttributes(NClist* attset)
94 {
95     unsigned int i,j;
96     for(i=0;i<nclistlength(attset);i++) {
97 	OCattribute* att = (OCattribute*)nclistget(attset,i);
98 	if(att->name != NULL) free(att->name);
99 	if(att->etype == OC_String || att->etype == OC_URL) {
100 	    for(j=0;j<att->nvalues;j++) {
101 		char* s = ((char**)att->values)[j];
102 		if(s != NULL) free(s);
103 	    }
104 	} else {
105 	    free(att->values);
106 	}
107     }
108 }
109 #endif
110 
111 #if 0
112 void
113 freeOCnode(OCnode* cdf, int deep)
114 {
115     unsigned int i;
116     if(cdf == NULL) return;
117     if(cdf->name != NULL) free(cdf->name);
118     if(cdf->fullname != NULL) free(cdf->fullname);
119     if(cdf->attributes != NULL) freeAttributes(cdf->attributes);
120     if(cdf->subnodes != NULL) {
121 	if(deep) {
122             for(i=0;i<nclistlength(cdf->subnodes);i++) {
123 	        OCnode* node = (OCnode*)nclistget(cdf->subnodes,i);
124 		freeOCnode(node,deep);
125 	    }
126 	}
127         nclistfree(cdf->subnodes);
128     }
129     free(cdf);
130 }
131 #endif
132 
133 int
ocfindbod(NCbytes * buffer,size_t * bodp,size_t * ddslenp)134 ocfindbod(NCbytes* buffer, size_t* bodp, size_t* ddslenp)
135 {
136     unsigned int i;
137     char* content;
138     size_t len = ncbyteslength(buffer);
139     const char** marks;
140 
141     content = ncbytescontents(buffer);
142 
143     for(marks = DDSdatamarks;*marks;marks++) {
144 	const char* mark = *marks;
145         size_t tlen = strlen(mark);
146         for(i=0;i<len;i++) {
147 	    if((i+tlen) <= len
148 	        && (ocstrncmp(content+i,mark,tlen)==0)) {
149 	       *ddslenp = i;
150 	        i += tlen;
151 	        *bodp = i;
152 	        return 1;
153 	    }
154 	}
155     }
156     *ddslenp = 0;
157     *bodp = 0;
158     return 0; /* tag not found; not necessarily an error*/
159 }
160 
161 /* Compute total # of elements if dimensioned*/
162 size_t
octotaldimsize(size_t rank,size_t * sizes)163 octotaldimsize(size_t rank, size_t* sizes)
164 {
165     unsigned int i;
166     size_t count = 1;
167     for(i=0;i<rank;i++) {
168         count *= sizes[i];
169     }
170     return count;
171 }
172 
173 size_t
octypesize(OCtype etype)174 octypesize(OCtype etype)
175 {
176     switch (etype) {
177     case OC_Char:	return sizeof(char);
178     case OC_Byte:	return sizeof(signed char);
179     case OC_UByte:	return sizeof(unsigned char);
180     case OC_Int16:	return sizeof(short);
181     case OC_UInt16:	return sizeof(unsigned short);
182     case OC_Int32:	return sizeof(int);
183     case OC_UInt32:	return sizeof(unsigned int);
184     case OC_Float32:	return sizeof(float);
185     case OC_Float64:	return sizeof(double);
186 #ifdef HAVE_LONG_LONG_INT
187     case OC_Int64:	return sizeof(long long);
188     case OC_UInt64:	return sizeof(unsigned long long);
189 #endif
190     case OC_String:	return sizeof(char*);
191     case OC_URL:	return sizeof(char*);
192     default: break;     /* Ignore all others */
193     }
194     return 0;
195 }
196 
197 char*
octypetostring(OCtype octype)198 octypetostring(OCtype octype)
199 {
200     switch (octype) {
201     case OC_NAT:          return "OC_NAT";
202     case OC_Char:         return "OC_Char";
203     case OC_Byte:         return "OC_Byte";
204     case OC_UByte:	   return "OC_UByte";
205     case OC_Int16:        return "OC_Int16";
206     case OC_UInt16:       return "OC_UInt16";
207     case OC_Int32:        return "OC_Int32";
208     case OC_UInt32:       return "OC_UInt32";
209     case OC_Int64:        return "OC_Int64";
210     case OC_UInt64:       return "OC_UInt64";
211     case OC_Float32:      return "OC_Float32";
212     case OC_Float64:      return "OC_Float64";
213     case OC_String:       return "OC_String";
214     case OC_URL:          return "OC_URL";
215     /* Non-primitives*/
216     case OC_Dataset:      return "OC_Dataset";
217     case OC_Sequence:     return "OC_Sequence";
218     case OC_Grid:         return "OC_Grid";
219     case OC_Structure:    return "OC_Structure";
220     case OC_Dimension:    return "OC_Dimension";
221     case OC_Attribute:    return "OC_Attribute";
222     case OC_Attributeset: return "OC_Attributeset";
223     case OC_Atomic:       return "OC_Atomic";
224     default: break;
225     }
226     return NULL;
227 }
228 
229 char*
octypetoddsstring(OCtype octype)230 octypetoddsstring(OCtype octype)
231 {
232     switch (octype) {
233     case OC_Byte:         return "Byte";
234     case OC_Int16:        return "Int16";
235     case OC_UInt16:       return "UInt16";
236     case OC_Int32:        return "Int32";
237     case OC_UInt32:       return "UInt32";
238     case OC_Float32:      return "Float32";
239     case OC_Float64:      return "Float64";
240     case OC_String:       return "String";
241     case OC_URL:          return "Url";
242     /* Non-atomics*/
243     case OC_Dataset:      return "Dataset";
244     case OC_Sequence:     return "Sequence";
245     case OC_Grid:         return "Grid";
246     case OC_Structure:    return "Structure";
247     case OC_Dimension:    return "Dimension";
248     case OC_Attribute:    return "Attribute";
249     case OC_Attributeset: return "Attributeset";
250     case OC_Atomic:       return "Atomic";
251     default: break;
252     }
253     return "<unknown>";
254 }
255 
256 
257 OCerror
octypeprint(OCtype etype,void * value,size_t bufsize,char * buf)258 octypeprint(OCtype etype, void* value, size_t bufsize, char* buf)
259 {
260     if(buf == NULL || bufsize == 0 || value == NULL) return OC_EINVAL;
261     buf[0] = '\0';
262     switch (etype) {
263     case OC_Char:
264 	snprintf(buf,bufsize,"'%c'",*(char*)value);
265 	break;
266     case OC_Byte:
267 	snprintf(buf,bufsize,"%d",*(signed char*)value);
268 	break;
269     case OC_UByte:
270 	snprintf(buf,bufsize,"%u",*(unsigned char*)value);
271 	break;
272     case OC_Int16:
273 	snprintf(buf,bufsize,"%d",*(short*)value);
274 	break;
275     case OC_UInt16:
276 	snprintf(buf,bufsize,"%u",*(unsigned short*)value);
277 	break;
278     case OC_Int32:
279 	snprintf(buf,bufsize,"%d",*(int*)value);
280 	break;
281     case OC_UInt32:
282 	snprintf(buf,bufsize,"%u",*(unsigned int*)value);
283 	break;
284     case OC_Float32:
285 	snprintf(buf,bufsize,"%g",*(float*)value);
286 	break;
287     case OC_Float64:
288 	snprintf(buf,bufsize,"%g",*(double*)value);
289 	break;
290 #ifdef HAVE_LONG_LONG_INT
291     case OC_Int64:
292 	snprintf(buf,bufsize,"%lld",*(long long*)value);
293 	break;
294     case OC_UInt64:
295 	snprintf(buf,bufsize,"%llu",*(unsigned long long*)value);
296 	break;
297 #endif
298     case OC_String:
299     case OC_URL: {
300 	char* s = *(char**)value;
301 	snprintf(buf,bufsize,"\"%s\"",s);
302 	} break;
303     default: break;
304     }
305     return OC_NOERR;
306 }
307 
308 size_t
xxdrsize(OCtype etype)309 xxdrsize(OCtype etype)
310 {
311     switch (etype) {
312     case OC_Char:
313     case OC_Byte:
314     case OC_UByte:
315     case OC_Int16:
316     case OC_UInt16:
317     case OC_Int32:
318     case OC_UInt32:
319 	return XDRUNIT;
320     case OC_Int64:
321     case OC_UInt64:
322 	return (2*XDRUNIT);
323     case OC_Float32:
324 	return XDRUNIT;
325     case OC_Float64:
326 	return (2*XDRUNIT);
327     case OC_String:
328     case OC_URL:
329     default: break;
330     }
331     return 0;
332 }
333 
334 /**************************************/
335 
336 char*
ocerrstring(int err)337 ocerrstring(int err)
338 {
339     if(err == 0) return "no error";
340     if(err > 0) return strerror(err);
341     switch (err) {
342 	case OC_EBADID:
343 	    return "OC_EBADID: Not a valid ID";
344 	case OC_EINVAL:
345 	    return "OC_EINVAL: Invalid argument";
346 	case OC_EPERM:
347 	    return "OC_EPERM: Write to read only";
348 	case OC_EINVALCOORDS:
349 	    return "OC_EINVALCOORDS: Index exceeds dimension bound";
350 	case OC_ENOTVAR:
351 	    return "OC_ENOTVAR: Variable not found";
352 	case OC_ECHAR:
353 	    return "OC_ECHAR: Attempt to convert between text & numbers";
354 	case OC_EEDGE:
355 	    return "OC_EEDGE: Start+count exceeds dimension bound";
356 	case OC_ESTRIDE:
357 	    return "OC_ESTRIDE: Illegal stride";
358 	case OC_ENOMEM:
359 	    return "OC_ENOMEM: Memory allocation (malloc) failure";
360 	case OC_EDIMSIZE:
361 	    return "OC_EDIMSIZE: Invalid dimension size";
362 	case OC_EDAP:
363 	    return "OC_EDAP: unspecified DAP failure";
364 	case OC_EXDR:
365 	    return "OC_EXDR: XDR failure";
366 	case OC_ECURL:
367 	    return "OC_ECURL: unspecified libcurl failure";
368 	case OC_EBADURL:
369 	    return "OC_EBADURL: malformed url";
370 	case OC_EBADVAR:
371 	    return "OC_EBADVAR: no such variable";
372 	case OC_EOPEN:
373 	    return "OC_EOPEN: temporary file open failed";
374 	case OC_EIO:
375 	    return "OC_EIO: I/O failure";
376 	case OC_ENODATA:
377 	    return "OC_ENODATA: Variable has no data in DAP request";
378 	case OC_EDAPSVC:
379 	    return "OC_EDAPSVC: DAP Server error";
380 	case OC_ENAMEINUSE:
381 	    return "OC_ENAMEINUSE: Duplicate name in DDS";
382 	case OC_EDAS:
383 	    return "OC_EDAS: Malformed or unreadable DAS";
384 	case OC_EDDS:
385 	    return "OC_EDDS: Malformed or unreadable DDS";
386 	case OC_EDATADDS:
387 	    return "OC_EDATADDS: Malformed or unreadable DATADDS";
388 	case OC_ERCFILE:
389 	    return "OC_ERCFILE: Malformed,  unreadable, or bad value in the run-time configuration file";
390 	case OC_ENOFILE:
391 	    return "OC_ENOFILE: cannot read content of URL";
392 
393 	/* oc_data related errors */
394 	case OC_EINDEX:
395 	    return "OC_EINDEX: index argument too large";
396 	case OC_EBADTYPE:
397 	    return "OC_EBADTYPE: argument of wrong OCtype";
398 
399 	/* String concatenation overrun */
400 	case OC_EOVERRUN:
401 	    return "OC_EOVERRUN: internal concatenation failed";
402 
403 	/* Authorization Error */
404 	case OC_EAUTH:
405 	    return "OC_EAUTH: authorization failure";
406 
407 	default: break;
408     }
409     return "<unknown error code>";
410 }
411 
412 OCerror
ocsvcerrordata(OCstate * state,char ** codep,char ** msgp,long * httpp)413 ocsvcerrordata(OCstate* state, char** codep, char** msgp, long* httpp)
414 {
415     if(codep) *codep = state->error.code;
416     if(msgp) *msgp = state->error.message;
417     if(httpp) *httpp = state->error.httpcode;
418     return OC_NOERR;
419 }
420 
421 /* if we get OC_EDATADDS error, then try to capture any
422    error message and log it; assumes that in this case,
423    the datadds is not big.
424 */
425 void
ocdataddsmsg(OCstate * state,OCtree * tree)426 ocdataddsmsg(OCstate* state, OCtree* tree)
427 {
428 #define ERRCHUNK 1024
429 #define ERRFILL ' '
430 #define ERRTAG "Error {"
431     int i,j;
432     size_t len;
433     XXDR* xdrs;
434     char* contents;
435     off_t ckp;
436 
437     if(tree == NULL) return;
438     /* get available space */
439     xdrs = tree->data.xdrs;
440     len = xxdr_length(xdrs);
441     if(len < strlen(ERRTAG))
442 	return; /* no room */
443     ckp = xxdr_getpos(xdrs);
444     xxdr_setpos(xdrs,(off_t)0);
445     /* read the whole thing */
446     contents = (char*)malloc(len+1);
447     (void)xxdr_getbytes(xdrs,contents,(off_t)len);
448     contents[len] = '\0';
449     /* Look for error tag */
450     for(i=0;i<len;i++) {
451         if(ocstrncmp(contents+i,ERRTAG,strlen(ERRTAG))==0) {
452 	    /* log the error message */
453 	    /* Do a quick and dirty escape */
454 	    for(j=i;j<len;j++) {
455 	        int c = contents[i+j];
456 		if(c > 0 && (c < ' ' || c >= '\177'))
457 		    contents[i+j] = ERRFILL;
458 	    }
459 	    nclog(NCLOGERR,"DATADDS failure, possible message: '%s'\n",
460 			contents+i);
461 	    goto done;
462 	}
463     }
464     xxdr_setpos(xdrs,ckp);
465 done:
466     return;
467 }
468 
469 /* Given some set of indices [i0][i1]...[in] (where n == rank-1)
470    and the maximum sizes, compute the linear offset
471    for set of dimension indices.
472 */
473 size_t
ocarrayoffset(size_t rank,size_t * sizes,const size_t * indices)474 ocarrayoffset(size_t rank, size_t* sizes, const size_t* indices)
475 {
476     unsigned int i;
477     size_t count = 0;
478     for(i=0;i<rank;i++) {
479 	count *= sizes[i];
480 	count += indices[i];
481     }
482     return count;
483 }
484 
485 /* Inverse of ocarrayoffset: convert linear index to a set of indices */
486 void
ocarrayindices(size_t index,size_t rank,size_t * sizes,size_t * indices)487 ocarrayindices(size_t index, size_t rank, size_t* sizes, size_t* indices)
488 {
489     int i;
490     for(i=rank-1;i>=0;i--) {
491 	indices[i] = index % sizes[i];
492 	index = (index - indices[i]) / sizes[i];
493     }
494 }
495 
496 /* Given some set of edge counts [i0][i1]...[in] (where n == rank-1)
497    and the maximum sizes, compute the linear offset
498    for the last edge position
499 */
500 size_t
ocedgeoffset(size_t rank,size_t * sizes,size_t * edges)501 ocedgeoffset(size_t rank, size_t* sizes, size_t* edges)
502 {
503     unsigned int i;
504     size_t count = 0;
505     for(i=0;i<rank;i++) {
506 	count *= sizes[i];
507 	count += (edges[i]-1);
508     }
509     return count;
510 }
511 
512 int
ocvalidateindices(size_t rank,size_t * sizes,size_t * indices)513 ocvalidateindices(size_t rank, size_t* sizes, size_t* indices)
514 {
515     int i;
516     for(i=0;i<rank;i++) {
517 	if(indices[i] >= sizes[i]) return 0;
518     }
519     return 1;
520 }
521 
522 int
oc_ispacked(OCnode * node)523 oc_ispacked(OCnode* node)
524 {
525     OCtype octype = node->octype;
526     OCtype etype = node->etype;
527     int isscalar = (node->array.rank == 0);
528     int packed;
529 
530     if(isscalar || octype != OC_Atomic)
531 	return 0; /* is not packed */
532     packed = (etype == OC_Byte
533 	      || etype == OC_UByte
534               || etype == OC_Char) ? 1 : 0;
535     return packed;
536 }
537 
538 /* Must be consistent with ocx.h.OCDT */
539 #define NMODES 6
540 #define MAXMODENAME 8 /*max (strlen(modestrings[i])) */
541 static const char* modestrings[NMODES+1] = {
542 "FIELD", /* ((OCDT)(1<<0)) field of a container */
543 "ELEMENT", /* ((OCDT)(1<<1)) element of a structure array */
544 "RECORD", /* ((OCDT)(1<<2)) record of a sequence */
545 "ARRAY", /* ((OCDT)(1<<3)) is structure array */
546 "SEQUENCE", /* ((OCDT)(1<<4)) is sequence */
547 "ATOMIC", /* ((OCDT)(1<<5)) is atomic leaf */
548 NULL,
549 };
550 
551 char*
ocdtmodestring(OCDT mode,int compact)552 ocdtmodestring(OCDT mode,int compact)
553 {
554     char* result = NULL;
555     int i;
556     char* p = NULL;
557 
558     result = malloc(1+(NMODES*(MAXMODENAME+1)));
559     if(result == NULL) return NULL;
560     p = result;
561     result[0] = '\0';
562     if(mode == 0) {
563 	if(compact) *p++ = '-';
564 	else if(!occoncat(result,sizeof(result),1,"NONE"))
565 	    return NULL;
566     } else for(i=0;;i++) {
567 	const char* ms = modestrings[i];
568 	if(ms == NULL) break;
569 	if(!compact && i > 0)
570 	    if(!occoncat(result,sizeof(result),1,","))
571 		return NULL;
572         if(fisset(mode,(1<<i))) {
573 	    if(compact) *p++ = ms[0];
574 	    else if(!occoncat(result,sizeof(result),1,ms))
575 		return NULL;
576 	}
577     }
578     /* pad compact list out to NMODES in length (+1 for null terminator) */
579     if(compact) {
580 	while((p-result) < NMODES) *p++ = ' ';
581 	*p = '\0';
582     }
583     return result;
584 }
585 
586 
587 /*
588 Instead of using snprintf to concatenate
589 multiple strings into a given target,
590 provide a direct concatenator.
591 So, this function concats the n argument strings
592 and overwrites the contents of dst.
593 Care is taken to never overrun the available
594 space (the size parameter).
595 Note that size is assumed to include the null
596 terminator and that in the event of overrun,
597 the string will have a null at dst[size-1].
598 Return 0 if overrun, 1 otherwise.
599 */
600 int
occopycat(char * dst,size_t size,size_t n,...)601 occopycat(char* dst, size_t size, size_t n, ...)
602 {
603     va_list args;
604     size_t avail = size - 1;
605     int i;
606     int status = 1; /* assume ok */
607     char* p = dst;
608 
609     if(n == 0) {
610 	if(size > 0)
611 	    dst[0] = '\0';
612 	return (size > 0 ? 1: 0);
613     }
614 
615     va_start(args,n);
616     for(i=0;i<n;i++) {
617 	char* q = va_arg(args, char*);
618 	for(;;) {
619 	    int c = *q++;
620 	    if(c == '\0') break;
621 	    if(avail == 0) {status = 0; goto done;}
622 	    *p++ = c;
623 	    avail--;
624 	}
625     }
626     /* make sure we null terminate;
627        note that since avail was size-1, there
628        will always be room
629     */
630     *p = '\0';
631 
632 done:
633     va_end(args);
634     return status;
635 }
636 
637 /*
638 Similar to occopycat, but
639 the n strings are, in effect,
640 concatenated and appended to the
641 current contents of dst.
642 The size parameter is the total size of dst,
643 including room for null terminator.
644 Return 0 if overrun, 1 otherwise.
645 */
646 int
occoncat(char * dst,size_t size,size_t n,...)647 occoncat(char* dst, size_t size, size_t n, ...)
648 {
649     va_list args;
650     int status = 1; /* assume ok */
651     size_t avail = 0;
652     int i;
653     char* p;
654     size_t dstused;
655     dstused = strlen(dst);
656     if(dstused >= size)
657 	return 0; /* There is no room to append */
658     /* move to the end of the current contents of dst
659        and act like we are doing copycat
660     */
661     p = dst + dstused;
662     size -= dstused;
663     avail = size - 1;
664     if(n == 0) {
665 	if(size > 0)
666 	    p[0] = '\0';
667 	return (size > 0 ? 1: 0);
668     }
669 
670     va_start(args,n);
671     for(i=0;i<n;i++) {
672 	char* q = va_arg(args, char*);
673 	for(;;) {
674 	    int c = *q++;
675 	    if(c == '\0') break;
676 	    if(avail == 0) {status = 0; goto done;}
677 	    *p++ = c;
678 	    avail--;
679 	}
680     }
681     /* make sure we null terminate;
682        note that since avail was size-1, there
683        will always be room
684     */
685     *p = '\0';
686 
687 done:
688     va_end(args);
689     return status;
690 }
691 
692 /* merge two envv style lists */
693 char**
ocmerge(const char ** list1,const char ** list2)694 ocmerge(const char** list1, const char** list2)
695 {
696     int l1, l2;
697     char** merge;
698     const char** p;
699     for(l1=0,p=list1;*p;p++) {l1++;}
700     for(l2=0,p=list2;*p;p++) {l2++;}
701     merge = (char**)malloc(sizeof(char*)*(l1+l2+1));
702     if(merge == NULL)
703 	return NULL;
704     memcpy(merge,list1,sizeof(char*)*l1);
705     memcpy(merge+l1,list2,sizeof(char*)*l2);
706     merge[l1+l2] = NULL;
707     return merge;
708 }
709