add a hack for Decap Attack
[picodrive.git] / unzip / unzip.c
1 #include "unzip.h"\r
2 \r
3 #include <stdlib.h>\r
4 #include <string.h>\r
5 #include <ctype.h>\r
6 #include <assert.h>\r
7 \r
8 #include <zlib.h>\r
9 \r
10 /* public globals */\r
11 //int   gUnzipQuiet = 0;                /* flag controls error messages */\r
12 \r
13 #define ERROR_CORRUPT "The zipfile seems to be corrupt, please check it"\r
14 #define ERROR_FILESYSTEM "Your filesystem seems to be corrupt, please check it"\r
15 #define ERROR_UNSUPPORTED "The format of this zipfile is not supported, please recompress it"\r
16 \r
17 #define INFLATE_INPUT_BUFFER_MAX 16384\r
18 #ifndef MIN\r
19 #define MIN(x,y) ((x)<(y)?(x):(y))\r
20 #endif\r
21 \r
22 \r
23 // notaz\r
24 #if 1 //def __DEBUG_PRINT\r
25 #define logerror printf\r
26 #define errormsg(str1,def,fname) printf("%s: " #def ": " str1 "\n", fname);\r
27 #else\r
28 #define logerror(x...)\r
29 #define errormsg(x...)\r
30 #endif\r
31 \r
32 /* Print a error message */\r
33 //void errormsg(const char* extmsg, const char* usermsg, const char* zipname) {\r
34         /* Output to the user with no internal detail */\r
35 //      if (!gUnzipQuiet)\r
36 //              printf("Error in zipfile %s\n%s\n", zipname, usermsg);\r
37         /* Output to log file with all informations */\r
38 //      logerror("Error in zipfile %s: %s\n", zipname, extmsg);\r
39 //      printf("Error in zipfile %s: %s\n", zipname, extmsg);\r
40 //}\r
41 \r
42 /* -------------------------------------------------------------------------\r
43    Unzip support\r
44  ------------------------------------------------------------------------- */\r
45 \r
46 /* Use these to avoid structure padding and byte-ordering problems */\r
47 static UINT16 read_word (char *buf) {\r
48    unsigned char *ubuf = (unsigned char *) buf;\r
49 \r
50    return ((UINT16)ubuf[1] << 8) | (UINT16)ubuf[0];\r
51 }\r
52 \r
53 /* Use these to avoid structure padding and byte-ordering problems */\r
54 static UINT32 read_dword (char *buf) {\r
55    unsigned char *ubuf = (unsigned char *) buf;\r
56 \r
57    return ((UINT32)ubuf[3] << 24) | ((UINT32)ubuf[2] << 16) | ((UINT32)ubuf[1] << 8) | (UINT32)ubuf[0];\r
58 }\r
59 \r
60 /* Locate end-of-central-dir sig in buffer and return offset\r
61    out:\r
62         *offset offset of cent dir start in buffer\r
63    return:\r
64         ==0 not found\r
65         !=0 found, *offset valid\r
66 */\r
67 static int ecd_find_sig (char *buffer, int buflen, int *offset)\r
68 {\r
69         static char ecdsig[] = { 'P', 'K', 0x05, 0x06 };\r
70         int i;\r
71         for (i=buflen-22; i>=0; i--) {\r
72                 if (memcmp(buffer+i, ecdsig, 4) == 0) {\r
73                         *offset = i;\r
74                         return 1;\r
75                 }\r
76         }\r
77         return 0;\r
78 }\r
79 \r
80 /* Read ecd data in zip structure\r
81    in:\r
82      zip->fp, zip->length zip file\r
83    out:\r
84      zip->ecd, zip->ecd_length ecd data\r
85 */\r
86 static int ecd_read(ZIP* zip) {\r
87         char* buf;\r
88         int buf_length = 1024; /* initial buffer length */\r
89 \r
90         while (1) {\r
91                 int offset;\r
92 \r
93                 if (buf_length > zip->length)\r
94                         buf_length = zip->length;\r
95 \r
96                 if (fseek(zip->fp, zip->length - buf_length, SEEK_SET) != 0) {\r
97                         return -1;\r
98                 }\r
99 \r
100                 /* allocate buffer */\r
101                 buf = (char*)malloc( buf_length );\r
102                 if (!buf) {\r
103                         return -1;\r
104                 }\r
105 \r
106                 if (fread( buf, buf_length, 1, zip->fp ) != 1) {\r
107                         free(buf);\r
108                         return -1;\r
109                 }\r
110 \r
111                 if (ecd_find_sig(buf, buf_length, &offset)) {\r
112                         zip->ecd_length = buf_length - offset;\r
113 \r
114                         zip->ecd = (char*)malloc( zip->ecd_length );\r
115                         if (!zip->ecd) {\r
116                                 free(buf);\r
117                                 return -1;\r
118                         }\r
119 \r
120                         memcpy(zip->ecd, buf + offset, zip->ecd_length);\r
121 \r
122                         free(buf);\r
123                         return 0;\r
124                 }\r
125 \r
126                 free(buf);\r
127 \r
128                 if (buf_length < zip->length) {\r
129                         /* double buffer */\r
130                         buf_length = 2*buf_length;\r
131 \r
132                         logerror("Retry reading of zip ecd for %d bytes\n",buf_length);\r
133 \r
134                 } else {\r
135                         return -1;\r
136                 }\r
137         }\r
138 }\r
139 \r
140 /* offsets in end of central directory structure */\r
141 #define ZIPESIG         0x00\r
142 #define ZIPEDSK         0x04\r
143 #define ZIPECEN         0x06\r
144 #define ZIPENUM         0x08\r
145 #define ZIPECENN        0x0a\r
146 #define ZIPECSZ         0x0c\r
147 #define ZIPEOFST        0x10\r
148 #define ZIPECOML        0x14\r
149 #define ZIPECOM         0x16\r
150 \r
151 /* offsets in central directory entry structure */\r
152 #define ZIPCENSIG       0x0\r
153 #define ZIPCVER         0x4\r
154 #define ZIPCOS          0x5\r
155 #define ZIPCVXT         0x6\r
156 #define ZIPCEXOS        0x7\r
157 #define ZIPCFLG         0x8\r
158 #define ZIPCMTHD        0xa\r
159 #define ZIPCTIM         0xc\r
160 #define ZIPCDAT         0xe\r
161 #define ZIPCCRC         0x10\r
162 #define ZIPCSIZ         0x14\r
163 #define ZIPCUNC         0x18\r
164 #define ZIPCFNL         0x1c\r
165 #define ZIPCXTL         0x1e\r
166 #define ZIPCCML         0x20\r
167 #define ZIPDSK          0x22\r
168 #define ZIPINT          0x24\r
169 #define ZIPEXT          0x26\r
170 #define ZIPOFST         0x2a\r
171 #define ZIPCFN          0x2e\r
172 \r
173 /* offsets in local file header structure */\r
174 #define ZIPLOCSIG       0x00\r
175 #define ZIPVER          0x04\r
176 #define ZIPGENFLG       0x06\r
177 #define ZIPMTHD         0x08\r
178 #define ZIPTIME         0x0a\r
179 #define ZIPDATE         0x0c\r
180 #define ZIPCRC          0x0e\r
181 #define ZIPSIZE         0x12\r
182 #define ZIPUNCMP        0x16\r
183 #define ZIPFNLN         0x1a\r
184 #define ZIPXTRALN       0x1c\r
185 #define ZIPNAME         0x1e\r
186 \r
187 /* Opens a zip stream for reading\r
188    return:\r
189      !=0 success, zip stream\r
190      ==0 error\r
191 */\r
192 ZIP* openzip(const char* zipfile) {\r
193         /* allocate */\r
194         ZIP* zip = (ZIP*)malloc( sizeof(ZIP) );\r
195         if (!zip) {\r
196                 return 0;\r
197         }\r
198 \r
199         /* open */\r
200         zip->fp = fopen(zipfile, "rb");\r
201         if (!zip->fp) {\r
202                 errormsg ("Opening for reading", ERROR_FILESYSTEM, zipfile);\r
203                 free(zip);\r
204                 return 0;\r
205         }\r
206 \r
207         /* go to end */\r
208         if (fseek(zip->fp, 0L, SEEK_END) != 0) {\r
209                 errormsg ("Seeking to end", ERROR_FILESYSTEM, zipfile);\r
210                 fclose(zip->fp);\r
211                 free(zip);\r
212                 return 0;\r
213         }\r
214 \r
215         /* get length */\r
216         zip->length = ftell(zip->fp);\r
217         if (zip->length < 0) {\r
218                 errormsg ("Get file size", ERROR_FILESYSTEM, zipfile);\r
219                 fclose(zip->fp);\r
220                 free(zip);\r
221                 return 0;\r
222         }\r
223         if (zip->length == 0) {\r
224                 errormsg ("Empty file", ERROR_CORRUPT, zipfile);\r
225                 fclose(zip->fp);\r
226                 free(zip);\r
227                 return 0;\r
228         }\r
229 \r
230         /* read ecd data */\r
231         if (ecd_read(zip)!=0) {\r
232                 errormsg ("Reading ECD (end of central directory)", ERROR_CORRUPT, zipfile);\r
233                 fclose(zip->fp);\r
234                 free(zip);\r
235                 return 0;\r
236         }\r
237 \r
238         /* compile ecd info */\r
239         zip->end_of_cent_dir_sig = read_dword (zip->ecd+ZIPESIG);\r
240         zip->number_of_this_disk = read_word (zip->ecd+ZIPEDSK);\r
241         zip->number_of_disk_start_cent_dir = read_word (zip->ecd+ZIPECEN);\r
242         zip->total_entries_cent_dir_this_disk = read_word (zip->ecd+ZIPENUM);\r
243         zip->total_entries_cent_dir = read_word (zip->ecd+ZIPECENN);\r
244         zip->size_of_cent_dir = read_dword (zip->ecd+ZIPECSZ);\r
245         zip->offset_to_start_of_cent_dir = read_dword (zip->ecd+ZIPEOFST);\r
246         zip->zipfile_comment_length = read_word (zip->ecd+ZIPECOML);\r
247         zip->zipfile_comment = zip->ecd+ZIPECOM;\r
248 \r
249         /* verify that we can work with this zipfile (no disk spanning allowed) */\r
250         if ((zip->number_of_this_disk != zip->number_of_disk_start_cent_dir) ||\r
251                 (zip->total_entries_cent_dir_this_disk != zip->total_entries_cent_dir) ||\r
252                 (zip->total_entries_cent_dir < 1)) {\r
253                 errormsg("Cannot span disks", ERROR_UNSUPPORTED, zipfile);\r
254                 free(zip->ecd);\r
255                 fclose(zip->fp);\r
256                 free(zip);\r
257                 return 0;\r
258         }\r
259 \r
260         if (fseek(zip->fp, zip->offset_to_start_of_cent_dir, SEEK_SET)!=0) {\r
261                 errormsg ("Seeking to central directory", ERROR_CORRUPT, zipfile);\r
262                 free(zip->ecd);\r
263                 fclose(zip->fp);\r
264                 free(zip);\r
265                 return 0;\r
266         }\r
267 \r
268         /* read from start of central directory */\r
269         zip->cd = (char*)malloc( zip->size_of_cent_dir );\r
270         if (!zip->cd) {\r
271                 free(zip->ecd);\r
272                 fclose(zip->fp);\r
273                 free(zip);\r
274                 return 0;\r
275         }\r
276 \r
277         if (fread(zip->cd, zip->size_of_cent_dir, 1, zip->fp)!=1) {\r
278                 errormsg ("Reading central directory", ERROR_CORRUPT, zipfile);\r
279                 free(zip->cd);\r
280                 free(zip->ecd);\r
281                 fclose(zip->fp);\r
282                 free(zip);\r
283                 return 0;\r
284         }\r
285 \r
286         /* reset ent */\r
287         zip->ent.name = 0;\r
288 \r
289         /* rewind */\r
290         zip->cd_pos = 0;\r
291 \r
292         /* file name */\r
293         zip->zip = (char*)malloc(strlen(zipfile)+1);\r
294         if (!zip->zip) {\r
295                 free(zip->cd);\r
296                 free(zip->ecd);\r
297                 fclose(zip->fp);\r
298                 free(zip);\r
299                 return 0;\r
300         }\r
301         strcpy(zip->zip, zipfile);\r
302 \r
303         return zip;\r
304 }\r
305 \r
306 /* Reads the current entry from a zip stream\r
307    in:\r
308      zip opened zip\r
309    return:\r
310      !=0 success\r
311      ==0 error\r
312 */\r
313 struct zipent* readzip(ZIP* zip) {\r
314 \r
315         /* end of directory */\r
316         if (zip->cd_pos >= zip->size_of_cent_dir)\r
317                 return 0;\r
318 \r
319         /* compile zipent info */\r
320         zip->ent.cent_file_header_sig = read_dword (zip->cd+zip->cd_pos+ZIPCENSIG);\r
321         zip->ent.version_made_by = *(zip->cd+zip->cd_pos+ZIPCVER);\r
322         zip->ent.host_os = *(zip->cd+zip->cd_pos+ZIPCOS);\r
323         zip->ent.version_needed_to_extract = *(zip->cd+zip->cd_pos+ZIPCVXT);\r
324         zip->ent.os_needed_to_extract = *(zip->cd+zip->cd_pos+ZIPCEXOS);\r
325         zip->ent.general_purpose_bit_flag = read_word (zip->cd+zip->cd_pos+ZIPCFLG);\r
326         zip->ent.compression_method = read_word (zip->cd+zip->cd_pos+ZIPCMTHD);\r
327         zip->ent.last_mod_file_time = read_word (zip->cd+zip->cd_pos+ZIPCTIM);\r
328         zip->ent.last_mod_file_date = read_word (zip->cd+zip->cd_pos+ZIPCDAT);\r
329         zip->ent.crc32 = read_dword (zip->cd+zip->cd_pos+ZIPCCRC);\r
330         zip->ent.compressed_size = read_dword (zip->cd+zip->cd_pos+ZIPCSIZ);\r
331         zip->ent.uncompressed_size = read_dword (zip->cd+zip->cd_pos+ZIPCUNC);\r
332         zip->ent.filename_length = read_word (zip->cd+zip->cd_pos+ZIPCFNL);\r
333         zip->ent.extra_field_length = read_word (zip->cd+zip->cd_pos+ZIPCXTL);\r
334         zip->ent.file_comment_length = read_word (zip->cd+zip->cd_pos+ZIPCCML);\r
335         zip->ent.disk_number_start = read_word (zip->cd+zip->cd_pos+ZIPDSK);\r
336         zip->ent.internal_file_attrib = read_word (zip->cd+zip->cd_pos+ZIPINT);\r
337         zip->ent.external_file_attrib = read_dword (zip->cd+zip->cd_pos+ZIPEXT);\r
338         zip->ent.offset_lcl_hdr_frm_frst_disk = read_dword (zip->cd+zip->cd_pos+ZIPOFST);\r
339 \r
340     /* check to see if filename length is illegally long (past the size of this directory\r
341        entry) */\r
342     if (zip->cd_pos + ZIPCFN + zip->ent.filename_length > zip->size_of_cent_dir)\r
343     {\r
344         errormsg("Invalid filename length in directory", ERROR_CORRUPT,zip->zip);\r
345         return 0;\r
346     }\r
347 \r
348         /* copy filename */\r
349         free(zip->ent.name);\r
350         zip->ent.name = (char*)malloc(zip->ent.filename_length + 1);\r
351         memcpy(zip->ent.name, zip->cd+zip->cd_pos+ZIPCFN, zip->ent.filename_length);\r
352         zip->ent.name[zip->ent.filename_length] = 0;\r
353 \r
354         /* skip to next entry in central dir */\r
355         zip->cd_pos += ZIPCFN + zip->ent.filename_length + zip->ent.extra_field_length + zip->ent.file_comment_length;\r
356 \r
357         return &zip->ent;\r
358 }\r
359 \r
360 /* Closes a zip stream */\r
361 void closezip(ZIP* zip) {\r
362         /* release all */\r
363         free(zip->ent.name);\r
364         free(zip->cd);\r
365         free(zip->ecd);\r
366         /* only if not suspended */\r
367         if (zip->fp)\r
368                 fclose(zip->fp);\r
369         free(zip->zip);\r
370         free(zip);\r
371 }\r
372 \r
373 /* Suspend access to a zip file (release file handler)\r
374    in:\r
375       zip opened zip\r
376    note:\r
377      A suspended zip is automatically reopened at first call of\r
378      readuncompressd() or readcompressed() functions\r
379 */\r
380 void suspendzip(ZIP* zip) {\r
381         if (zip->fp) {\r
382                 fclose(zip->fp);\r
383                 zip->fp = 0;\r
384         }\r
385 }\r
386 \r
387 /* Revive a suspended zip file (reopen file handler)\r
388    in:\r
389      zip suspended zip\r
390    return:\r
391         zip success\r
392         ==0 error (zip must be closed with closezip)\r
393 */\r
394 static ZIP* revivezip(ZIP* zip) {\r
395         if (!zip->fp) {\r
396                 zip->fp = fopen(zip->zip, "rb");\r
397                 if (!zip->fp) {\r
398                         return 0;\r
399                 }\r
400         }\r
401         return zip;\r
402 \r
403 }\r
404 \r
405 /* Reset a zip stream to the first entry\r
406    in:\r
407      zip opened zip\r
408    note:\r
409      ZIP file must be opened and not suspended\r
410 */\r
411 void rewindzip(ZIP* zip) {\r
412         zip->cd_pos = 0;\r
413 }\r
414 \r
415 /* Seek zip->fp to compressed data\r
416    return:\r
417         ==0 success\r
418         <0 error\r
419 */\r
420 int seekcompresszip(ZIP* zip, struct zipent* ent) {\r
421         char buf[ZIPNAME];\r
422         long offset;\r
423 \r
424         if (!zip->fp) {\r
425                 if (!revivezip(zip))\r
426                         return -1;\r
427         }\r
428 \r
429         if (fseek(zip->fp, ent->offset_lcl_hdr_frm_frst_disk, SEEK_SET)!=0) {\r
430                 errormsg ("Seeking to header", ERROR_CORRUPT, zip->zip);\r
431                 return -1;\r
432         }\r
433 \r
434         if (fread(buf, ZIPNAME, 1, zip->fp)!=1) {\r
435                 errormsg ("Reading header", ERROR_CORRUPT, zip->zip);\r
436                 return -1;\r
437         }\r
438 \r
439         {\r
440                 UINT16 filename_length = read_word (buf+ZIPFNLN);\r
441                 UINT16 extra_field_length = read_word (buf+ZIPXTRALN);\r
442 \r
443                 /* calculate offset to data and fseek() there */\r
444                 offset = ent->offset_lcl_hdr_frm_frst_disk + ZIPNAME + filename_length + extra_field_length;\r
445 \r
446                 if (fseek(zip->fp, offset, SEEK_SET) != 0) {\r
447                         errormsg ("Seeking to compressed data", ERROR_CORRUPT, zip->zip);\r
448                         return -1;\r
449                 }\r
450 \r
451         }\r
452 \r
453         return 0;\r
454 }\r
455 \r
456 /* Inflate a file\r
457    in:\r
458    in_file stream to inflate\r
459    in_size size of the compressed data to read\r
460    out_size size of decompressed data\r
461    out:\r
462    out_data buffer for decompressed data\r
463    return:\r
464    ==0 ok\r
465 \r
466    990525 rewritten for use with zlib MLR\r
467 */\r
468 static int inflate_file(FILE* in_file, unsigned in_size, unsigned char* out_data, unsigned out_size)\r
469 {\r
470     int err;\r
471         unsigned char* in_buffer;\r
472     z_stream d_stream; /* decompression stream */\r
473 \r
474     d_stream.zalloc = 0;\r
475     d_stream.zfree = 0;\r
476     d_stream.opaque = 0;\r
477 \r
478         d_stream.next_in  = 0;\r
479         d_stream.avail_in = 0;\r
480     d_stream.next_out = out_data;\r
481     d_stream.avail_out = out_size;\r
482 \r
483     err = inflateInit2(&d_stream, -MAX_WBITS);\r
484         /* windowBits is passed < 0 to tell that there is no zlib header.\r
485          * Note that in this case inflate *requires* an extra "dummy" byte\r
486          * after the compressed stream in order to complete decompression and\r
487          * return Z_STREAM_END.\r
488          */\r
489     if (err != Z_OK)\r
490         {\r
491                 logerror("inflateInit error: %d\n", err);\r
492         return -1;\r
493         }\r
494 \r
495         in_buffer = (unsigned char*)malloc(INFLATE_INPUT_BUFFER_MAX+1);\r
496         if (!in_buffer)\r
497                 return -1;\r
498 \r
499     for (;;)\r
500         {\r
501                 if (in_size <= 0)\r
502                 {\r
503                         logerror("inflate error: compressed size too small\n");\r
504                         free (in_buffer);\r
505                         return -1;\r
506                 }\r
507                 d_stream.next_in  = in_buffer;\r
508                 d_stream.avail_in = fread (in_buffer, 1, MIN(in_size, INFLATE_INPUT_BUFFER_MAX), in_file);\r
509                 in_size -= d_stream.avail_in;\r
510                 if (in_size == 0)\r
511                         d_stream.avail_in++; /* add dummy byte at end of compressed data */\r
512 \r
513         err = inflate(&d_stream, Z_NO_FLUSH);\r
514         if (err == Z_STREAM_END)\r
515                         break;\r
516                 if (err != Z_OK)\r
517                 {\r
518                         logerror("inflate error: %d\n", err);\r
519                         free (in_buffer);\r
520                         return -1;\r
521                 }\r
522     }\r
523 \r
524     err = inflateEnd(&d_stream);\r
525         if (err != Z_OK)\r
526         {\r
527                 logerror("inflateEnd error: %d\n", err);\r
528                 free (in_buffer);\r
529                 return -1;\r
530         }\r
531 \r
532         free (in_buffer);\r
533 \r
534         if ((d_stream.avail_out > 0) || (in_size > 0))\r
535         {\r
536                 logerror("zip size mismatch. %i\n", in_size);\r
537                 return -1;\r
538         }\r
539 \r
540         return 0;\r
541 }\r
542 \r
543 /* Read compressed data\r
544    out:\r
545         data compressed data read\r
546    return:\r
547         ==0 success\r
548         <0 error\r
549 */\r
550 int readcompresszip(ZIP* zip, struct zipent* ent, char* data) {\r
551         int err = seekcompresszip(zip,ent);\r
552         if (err!=0)\r
553                 return err;\r
554 \r
555         if (fread(data, ent->compressed_size, 1, zip->fp)!=1) {\r
556                 errormsg ("Reading compressed data", ERROR_CORRUPT, zip->zip);\r
557                 return -1;\r
558         }\r
559 \r
560         return 0;\r
561 }\r
562 \r
563 /* Read UNcompressed data\r
564    out:\r
565         data UNcompressed data\r
566    return:\r
567         ==0 success\r
568         <0 error\r
569 */\r
570 int readuncompresszip(ZIP* zip, struct zipent* ent, char* data) {\r
571         if (ent->compression_method == 0x0000) {\r
572                 /* file is not compressed, simply stored */\r
573 \r
574                 /* check if size are equal */\r
575                 if (ent->compressed_size != ent->uncompressed_size) {\r
576                         errormsg("Wrong uncompressed size in store compression", ERROR_CORRUPT,zip->zip);\r
577                         return -3;\r
578                 }\r
579 \r
580                 return readcompresszip(zip,ent,data);\r
581         } else if (ent->compression_method == 0x0008) {\r
582                 /* file is compressed using "Deflate" method */\r
583                 if (ent->version_needed_to_extract > 0x14) {\r
584                         errormsg("Version too new", ERROR_UNSUPPORTED,zip->zip);\r
585                         return -2;\r
586                 }\r
587 \r
588                 if (ent->os_needed_to_extract != 0x00) {\r
589                         errormsg("OS not supported", ERROR_UNSUPPORTED,zip->zip);\r
590                         return -2;\r
591                 }\r
592 \r
593                 if (ent->disk_number_start != zip->number_of_this_disk) {\r
594                         errormsg("Cannot span disks", ERROR_UNSUPPORTED,zip->zip);\r
595                         return -2;\r
596                 }\r
597 \r
598                 /* read compressed data */\r
599                 if (seekcompresszip(zip,ent)!=0) {\r
600                         return -1;\r
601                 }\r
602 \r
603                 /* configure inflate */\r
604                 if (inflate_file( zip->fp, ent->compressed_size, (unsigned char*)data, ent->uncompressed_size))\r
605                 {\r
606                         errormsg("Inflating compressed data", ERROR_CORRUPT, zip->zip);\r
607                         return -3;\r
608                 }\r
609 \r
610                 return 0;\r
611         } else {\r
612                 errormsg("Compression method unsupported", ERROR_UNSUPPORTED, zip->zip);\r
613                 return -2;\r
614         }\r
615 }\r