1 |
/* minigzip.c -- simulate gzip using the zlib compression library |
2 |
* Copyright (C) 1995-2006, 2010, 2011 Jean-loup Gailly. |
3 |
* For conditions of distribution and use, see copyright notice in zlib.h |
4 |
*/ |
5 |
|
6 |
/* |
7 |
* minigzip is a minimal implementation of the gzip utility. This is |
8 |
* only an example of using zlib and isn't meant to replace the |
9 |
* full-featured gzip. No attempt is made to deal with file systems |
10 |
* limiting names to 14 or 8+3 characters, etc... Error checking is |
11 |
* very limited. So use minigzip only for testing; use gzip for the |
12 |
* real thing. On MSDOS, use only on file names without extension |
13 |
* or in pipe mode. |
14 |
*/ |
15 |
|
16 |
/* @(#) $Id$ */ |
17 |
|
18 |
#include "zlib.h" |
19 |
#include <stdio.h> |
20 |
|
21 |
#ifdef STDC |
22 |
# include <string.h> |
23 |
# include <stdlib.h> |
24 |
#endif |
25 |
|
26 |
#ifdef USE_MMAP |
27 |
# include <sys/types.h> |
28 |
# include <sys/mman.h> |
29 |
# include <sys/stat.h> |
30 |
#endif |
31 |
|
32 |
#if defined(MSDOS) || defined(OS2) || defined(WIN32) || defined(__CYGWIN__) |
33 |
# include <fcntl.h> |
34 |
# include <io.h> |
35 |
# ifdef UNDER_CE |
36 |
# include <stdlib.h> |
37 |
# endif |
38 |
# define SET_BINARY_MODE(file) setmode(fileno(file), O_BINARY) |
39 |
#else |
40 |
# define SET_BINARY_MODE(file) |
41 |
#endif |
42 |
|
43 |
#ifdef _MSC_VER |
44 |
# define snprintf _snprintf |
45 |
#endif |
46 |
|
47 |
#ifdef VMS |
48 |
# define unlink delete |
49 |
# define GZ_SUFFIX "-gz" |
50 |
#endif |
51 |
#ifdef RISCOS |
52 |
# define unlink remove |
53 |
# define GZ_SUFFIX "-gz" |
54 |
# define fileno(file) file->__file |
55 |
#endif |
56 |
#if defined(__MWERKS__) && __dest_os != __be_os && __dest_os != __win32_os |
57 |
# include <unix.h> /* for fileno */ |
58 |
#endif |
59 |
|
60 |
#if !defined(Z_HAVE_UNISTD_H) && !defined(_LARGEFILE64_SOURCE) |
61 |
#ifndef WIN32 /* unlink already in stdio.h for WIN32 */ |
62 |
extern int unlink OF((const char *)); |
63 |
#endif |
64 |
#endif |
65 |
|
66 |
#if defined(UNDER_CE) |
67 |
# include <windows.h> |
68 |
# define perror(s) pwinerror(s) |
69 |
|
70 |
/* Map the Windows error number in ERROR to a locale-dependent error |
71 |
message string and return a pointer to it. Typically, the values |
72 |
for ERROR come from GetLastError. |
73 |
|
74 |
The string pointed to shall not be modified by the application, |
75 |
but may be overwritten by a subsequent call to strwinerror |
76 |
|
77 |
The strwinerror function does not change the current setting |
78 |
of GetLastError. */ |
79 |
|
80 |
static char *strwinerror (error) |
81 |
DWORD error; |
82 |
{ |
83 |
static char buf[1024]; |
84 |
|
85 |
wchar_t *msgbuf; |
86 |
DWORD lasterr = GetLastError(); |
87 |
DWORD chars = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM |
88 |
| FORMAT_MESSAGE_ALLOCATE_BUFFER, |
89 |
NULL, |
90 |
error, |
91 |
0, /* Default language */ |
92 |
(LPVOID)&msgbuf, |
93 |
0, |
94 |
NULL); |
95 |
if (chars != 0) { |
96 |
/* If there is an \r\n appended, zap it. */ |
97 |
if (chars >= 2 |
98 |
&& msgbuf[chars - 2] == '\r' && msgbuf[chars - 1] == '\n') { |
99 |
chars -= 2; |
100 |
msgbuf[chars] = 0; |
101 |
} |
102 |
|
103 |
if (chars > sizeof (buf) - 1) { |
104 |
chars = sizeof (buf) - 1; |
105 |
msgbuf[chars] = 0; |
106 |
} |
107 |
|
108 |
wcstombs(buf, msgbuf, chars + 1); |
109 |
LocalFree(msgbuf); |
110 |
} |
111 |
else { |
112 |
sprintf(buf, "unknown win32 error (%ld)", error); |
113 |
} |
114 |
|
115 |
SetLastError(lasterr); |
116 |
return buf; |
117 |
} |
118 |
|
119 |
static void pwinerror (s) |
120 |
const char *s; |
121 |
{ |
122 |
if (s && *s) |
123 |
fprintf(stderr, "%s: %s\n", s, strwinerror(GetLastError ())); |
124 |
else |
125 |
fprintf(stderr, "%s\n", strwinerror(GetLastError ())); |
126 |
} |
127 |
|
128 |
#endif /* UNDER_CE */ |
129 |
|
130 |
#ifndef GZ_SUFFIX |
131 |
# define GZ_SUFFIX ".gz" |
132 |
#endif |
133 |
#define SUFFIX_LEN (sizeof(GZ_SUFFIX)-1) |
134 |
|
135 |
#define BUFLEN 16384 |
136 |
#define MAX_NAME_LEN 1024 |
137 |
|
138 |
#ifdef MAXSEG_64K |
139 |
# define local static |
140 |
/* Needed for systems with limitation on stack size. */ |
141 |
#else |
142 |
# define local |
143 |
#endif |
144 |
|
145 |
#ifdef Z_SOLO |
146 |
/* for Z_SOLO, create simplified gz* functions using deflate and inflate */ |
147 |
|
148 |
#if defined(Z_HAVE_UNISTD_H) || defined(Z_LARGE) |
149 |
# include <unistd.h> /* for unlink() */ |
150 |
#endif |
151 |
|
152 |
void *myalloc OF((void *, unsigned, unsigned)); |
153 |
void myfree OF((void *, void *)); |
154 |
|
155 |
void *myalloc(q, n, m) |
156 |
void *q; |
157 |
unsigned n, m; |
158 |
{ |
159 |
q = Z_NULL; |
160 |
return calloc(n, m); |
161 |
} |
162 |
|
163 |
void myfree(q, p) |
164 |
void *q, *p; |
165 |
{ |
166 |
q = Z_NULL; |
167 |
free(p); |
168 |
} |
169 |
|
170 |
typedef struct gzFile_s { |
171 |
FILE *file; |
172 |
int write; |
173 |
int err; |
174 |
char *msg; |
175 |
z_stream strm; |
176 |
} *gzFile; |
177 |
|
178 |
gzFile gzopen OF((const char *, const char *)); |
179 |
gzFile gzdopen OF((int, const char *)); |
180 |
gzFile gz_open OF((const char *, int, const char *)); |
181 |
|
182 |
gzFile gzopen(path, mode) |
183 |
const char *path; |
184 |
const char *mode; |
185 |
{ |
186 |
return gz_open(path, -1, mode); |
187 |
} |
188 |
|
189 |
gzFile gzdopen(fd, mode) |
190 |
int fd; |
191 |
const char *mode; |
192 |
{ |
193 |
return gz_open(NULL, fd, mode); |
194 |
} |
195 |
|
196 |
gzFile gz_open(path, fd, mode) |
197 |
const char *path; |
198 |
int fd; |
199 |
const char *mode; |
200 |
{ |
201 |
gzFile gz; |
202 |
int ret; |
203 |
|
204 |
gz = malloc(sizeof(struct gzFile_s)); |
205 |
if (gz == NULL) |
206 |
return NULL; |
207 |
gz->write = strchr(mode, 'w') != NULL; |
208 |
gz->strm.zalloc = myalloc; |
209 |
gz->strm.zfree = myfree; |
210 |
gz->strm.opaque = Z_NULL; |
211 |
if (gz->write) |
212 |
ret = deflateInit2(&(gz->strm), -1, 8, 15 + 16, 8, 0); |
213 |
else { |
214 |
gz->strm.next_in = 0; |
215 |
gz->strm.avail_in = Z_NULL; |
216 |
ret = inflateInit2(&(gz->strm), 15 + 16); |
217 |
} |
218 |
if (ret != Z_OK) { |
219 |
free(gz); |
220 |
return NULL; |
221 |
} |
222 |
gz->file = path == NULL ? fdopen(fd, gz->write ? "wb" : "rb") : |
223 |
fopen(path, gz->write ? "wb" : "rb"); |
224 |
if (gz->file == NULL) { |
225 |
gz->write ? deflateEnd(&(gz->strm)) : inflateEnd(&(gz->strm)); |
226 |
free(gz); |
227 |
return NULL; |
228 |
} |
229 |
gz->err = 0; |
230 |
gz->msg = ""; |
231 |
return gz; |
232 |
} |
233 |
|
234 |
int gzwrite OF((gzFile, const void *, unsigned)); |
235 |
|
236 |
int gzwrite(gz, buf, len) |
237 |
gzFile gz; |
238 |
const void *buf; |
239 |
unsigned len; |
240 |
{ |
241 |
z_stream *strm; |
242 |
unsigned char out[BUFLEN]; |
243 |
|
244 |
if (gz == NULL || !gz->write) |
245 |
return 0; |
246 |
strm = &(gz->strm); |
247 |
strm->next_in = (void *)buf; |
248 |
strm->avail_in = len; |
249 |
do { |
250 |
strm->next_out = out; |
251 |
strm->avail_out = BUFLEN; |
252 |
(void)deflate(strm, Z_NO_FLUSH); |
253 |
fwrite(out, 1, BUFLEN - strm->avail_out, gz->file); |
254 |
} while (strm->avail_out == 0); |
255 |
return len; |
256 |
} |
257 |
|
258 |
int gzread OF((gzFile, void *, unsigned)); |
259 |
|
260 |
int gzread(gz, buf, len) |
261 |
gzFile gz; |
262 |
void *buf; |
263 |
unsigned len; |
264 |
{ |
265 |
int ret; |
266 |
unsigned got; |
267 |
unsigned char in[1]; |
268 |
z_stream *strm; |
269 |
|
270 |
if (gz == NULL || gz->write) |
271 |
return 0; |
272 |
if (gz->err) |
273 |
return 0; |
274 |
strm = &(gz->strm); |
275 |
strm->next_out = (void *)buf; |
276 |
strm->avail_out = len; |
277 |
do { |
278 |
got = fread(in, 1, 1, gz->file); |
279 |
if (got == 0) |
280 |
break; |
281 |
strm->next_in = in; |
282 |
strm->avail_in = 1; |
283 |
ret = inflate(strm, Z_NO_FLUSH); |
284 |
if (ret == Z_DATA_ERROR) { |
285 |
gz->err = Z_DATA_ERROR; |
286 |
gz->msg = strm->msg; |
287 |
return 0; |
288 |
} |
289 |
if (ret == Z_STREAM_END) |
290 |
inflateReset(strm); |
291 |
} while (strm->avail_out); |
292 |
return len - strm->avail_out; |
293 |
} |
294 |
|
295 |
int gzclose OF((gzFile)); |
296 |
|
297 |
int gzclose(gz) |
298 |
gzFile gz; |
299 |
{ |
300 |
z_stream *strm; |
301 |
unsigned char out[BUFLEN]; |
302 |
|
303 |
if (gz == NULL) |
304 |
return Z_STREAM_ERROR; |
305 |
strm = &(gz->strm); |
306 |
if (gz->write) { |
307 |
strm->next_in = Z_NULL; |
308 |
strm->avail_in = 0; |
309 |
do { |
310 |
strm->next_out = out; |
311 |
strm->avail_out = BUFLEN; |
312 |
(void)deflate(strm, Z_FINISH); |
313 |
fwrite(out, 1, BUFLEN - strm->avail_out, gz->file); |
314 |
} while (strm->avail_out == 0); |
315 |
deflateEnd(strm); |
316 |
} |
317 |
else |
318 |
inflateEnd(strm); |
319 |
fclose(gz->file); |
320 |
free(gz); |
321 |
return Z_OK; |
322 |
} |
323 |
|
324 |
const char *gzerror OF((gzFile, int *)); |
325 |
|
326 |
const char *gzerror(gz, err) |
327 |
gzFile gz; |
328 |
int *err; |
329 |
{ |
330 |
*err = gz->err; |
331 |
return gz->msg; |
332 |
} |
333 |
|
334 |
#endif |
335 |
|
336 |
char *prog; |
337 |
|
338 |
void error OF((const char *msg)); |
339 |
void gz_compress OF((FILE *in, gzFile out)); |
340 |
#ifdef USE_MMAP |
341 |
int gz_compress_mmap OF((FILE *in, gzFile out)); |
342 |
#endif |
343 |
void gz_uncompress OF((gzFile in, FILE *out)); |
344 |
void file_compress OF((char *file, char *mode)); |
345 |
void file_uncompress OF((char *file)); |
346 |
int main OF((int argc, char *argv[])); |
347 |
|
348 |
/* =========================================================================== |
349 |
* Display error message and exit |
350 |
*/ |
351 |
void error(msg) |
352 |
const char *msg; |
353 |
{ |
354 |
fprintf(stderr, "%s: %s\n", prog, msg); |
355 |
exit(1); |
356 |
} |
357 |
|
358 |
/* =========================================================================== |
359 |
* Compress input to output then close both files. |
360 |
*/ |
361 |
|
362 |
void gz_compress(in, out) |
363 |
FILE *in; |
364 |
gzFile out; |
365 |
{ |
366 |
local char buf[BUFLEN]; |
367 |
int len; |
368 |
int err; |
369 |
|
370 |
#ifdef USE_MMAP |
371 |
/* Try first compressing with mmap. If mmap fails (minigzip used in a |
372 |
* pipe), use the normal fread loop. |
373 |
*/ |
374 |
if (gz_compress_mmap(in, out) == Z_OK) return; |
375 |
#endif |
376 |
for (;;) { |
377 |
len = (int)fread(buf, 1, sizeof(buf), in); |
378 |
if (ferror(in)) { |
379 |
perror("fread"); |
380 |
exit(1); |
381 |
} |
382 |
if (len == 0) break; |
383 |
|
384 |
if (gzwrite(out, buf, (unsigned)len) != len) error(gzerror(out, &err)); |
385 |
} |
386 |
fclose(in); |
387 |
if (gzclose(out) != Z_OK) error("failed gzclose"); |
388 |
} |
389 |
|
390 |
#ifdef USE_MMAP /* MMAP version, Miguel Albrecht <malbrech@eso.org> */ |
391 |
|
392 |
/* Try compressing the input file at once using mmap. Return Z_OK if |
393 |
* if success, Z_ERRNO otherwise. |
394 |
*/ |
395 |
int gz_compress_mmap(in, out) |
396 |
FILE *in; |
397 |
gzFile out; |
398 |
{ |
399 |
int len; |
400 |
int err; |
401 |
int ifd = fileno(in); |
402 |
caddr_t buf; /* mmap'ed buffer for the entire input file */ |
403 |
off_t buf_len; /* length of the input file */ |
404 |
struct stat sb; |
405 |
|
406 |
/* Determine the size of the file, needed for mmap: */ |
407 |
if (fstat(ifd, &sb) < 0) return Z_ERRNO; |
408 |
buf_len = sb.st_size; |
409 |
if (buf_len <= 0) return Z_ERRNO; |
410 |
|
411 |
/* Now do the actual mmap: */ |
412 |
buf = mmap((caddr_t) 0, buf_len, PROT_READ, MAP_SHARED, ifd, (off_t)0); |
413 |
if (buf == (caddr_t)(-1)) return Z_ERRNO; |
414 |
|
415 |
/* Compress the whole file at once: */ |
416 |
len = gzwrite(out, (char *)buf, (unsigned)buf_len); |
417 |
|
418 |
if (len != (int)buf_len) error(gzerror(out, &err)); |
419 |
|
420 |
munmap(buf, buf_len); |
421 |
fclose(in); |
422 |
if (gzclose(out) != Z_OK) error("failed gzclose"); |
423 |
return Z_OK; |
424 |
} |
425 |
#endif /* USE_MMAP */ |
426 |
|
427 |
/* =========================================================================== |
428 |
* Uncompress input to output then close both files. |
429 |
*/ |
430 |
void gz_uncompress(in, out) |
431 |
gzFile in; |
432 |
FILE *out; |
433 |
{ |
434 |
local char buf[BUFLEN]; |
435 |
int len; |
436 |
int err; |
437 |
|
438 |
for (;;) { |
439 |
len = gzread(in, buf, sizeof(buf)); |
440 |
if (len < 0) error (gzerror(in, &err)); |
441 |
if (len == 0) break; |
442 |
|
443 |
if ((int)fwrite(buf, 1, (unsigned)len, out) != len) { |
444 |
error("failed fwrite"); |
445 |
} |
446 |
} |
447 |
if (fclose(out)) error("failed fclose"); |
448 |
|
449 |
if (gzclose(in) != Z_OK) error("failed gzclose"); |
450 |
} |
451 |
|
452 |
|
453 |
/* =========================================================================== |
454 |
* Compress the given file: create a corresponding .gz file and remove the |
455 |
* original. |
456 |
*/ |
457 |
void file_compress(file, mode) |
458 |
char *file; |
459 |
char *mode; |
460 |
{ |
461 |
local char outfile[MAX_NAME_LEN]; |
462 |
FILE *in; |
463 |
gzFile out; |
464 |
|
465 |
if (strlen(file) + strlen(GZ_SUFFIX) >= sizeof(outfile)) { |
466 |
fprintf(stderr, "%s: filename too long\n", prog); |
467 |
exit(1); |
468 |
} |
469 |
|
470 |
#if !defined(NO_snprintf) && !defined(NO_vsnprintf) |
471 |
snprintf(outfile, sizeof(outfile), "%s%s", file, GZ_SUFFIX); |
472 |
#else |
473 |
strcpy(outfile, file); |
474 |
strcat(outfile, GZ_SUFFIX); |
475 |
#endif |
476 |
|
477 |
in = fopen(file, "rb"); |
478 |
if (in == NULL) { |
479 |
perror(file); |
480 |
exit(1); |
481 |
} |
482 |
out = gzopen(outfile, mode); |
483 |
if (out == NULL) { |
484 |
fprintf(stderr, "%s: can't gzopen %s\n", prog, outfile); |
485 |
exit(1); |
486 |
} |
487 |
gz_compress(in, out); |
488 |
|
489 |
unlink(file); |
490 |
} |
491 |
|
492 |
|
493 |
/* =========================================================================== |
494 |
* Uncompress the given file and remove the original. |
495 |
*/ |
496 |
void file_uncompress(file) |
497 |
char *file; |
498 |
{ |
499 |
local char buf[MAX_NAME_LEN]; |
500 |
char *infile, *outfile; |
501 |
FILE *out; |
502 |
gzFile in; |
503 |
size_t len = strlen(file); |
504 |
|
505 |
if (len + strlen(GZ_SUFFIX) >= sizeof(buf)) { |
506 |
fprintf(stderr, "%s: filename too long\n", prog); |
507 |
exit(1); |
508 |
} |
509 |
|
510 |
#if !defined(NO_snprintf) && !defined(NO_vsnprintf) |
511 |
snprintf(buf, sizeof(buf), "%s", file); |
512 |
#else |
513 |
strcpy(buf, file); |
514 |
#endif |
515 |
|
516 |
if (len > SUFFIX_LEN && strcmp(file+len-SUFFIX_LEN, GZ_SUFFIX) == 0) { |
517 |
infile = file; |
518 |
outfile = buf; |
519 |
outfile[len-3] = '\0'; |
520 |
} else { |
521 |
outfile = file; |
522 |
infile = buf; |
523 |
#if !defined(NO_snprintf) && !defined(NO_vsnprintf) |
524 |
snprintf(buf + len, sizeof(buf) - len, "%s", GZ_SUFFIX); |
525 |
#else |
526 |
strcat(infile, GZ_SUFFIX); |
527 |
#endif |
528 |
} |
529 |
in = gzopen(infile, "rb"); |
530 |
if (in == NULL) { |
531 |
fprintf(stderr, "%s: can't gzopen %s\n", prog, infile); |
532 |
exit(1); |
533 |
} |
534 |
out = fopen(outfile, "wb"); |
535 |
if (out == NULL) { |
536 |
perror(file); |
537 |
exit(1); |
538 |
} |
539 |
|
540 |
gz_uncompress(in, out); |
541 |
|
542 |
unlink(infile); |
543 |
} |
544 |
|
545 |
|
546 |
/* =========================================================================== |
547 |
* Usage: minigzip [-c] [-d] [-f] [-h] [-r] [-1 to -9] [files...] |
548 |
* -c : write to standard output |
549 |
* -d : decompress |
550 |
* -f : compress with Z_FILTERED |
551 |
* -h : compress with Z_HUFFMAN_ONLY |
552 |
* -r : compress with Z_RLE |
553 |
* -1 to -9 : compression level |
554 |
*/ |
555 |
|
556 |
int main(argc, argv) |
557 |
int argc; |
558 |
char *argv[]; |
559 |
{ |
560 |
int copyout = 0; |
561 |
int uncompr = 0; |
562 |
gzFile file; |
563 |
char *bname, outmode[20]; |
564 |
|
565 |
#if !defined(NO_snprintf) && !defined(NO_vsnprintf) |
566 |
snprintf(outmode, sizeof(outmode), "%s", "wb6 "); |
567 |
#else |
568 |
strcpy(outmode, "wb6 "); |
569 |
#endif |
570 |
|
571 |
prog = argv[0]; |
572 |
bname = strrchr(argv[0], '/'); |
573 |
if (bname) |
574 |
bname++; |
575 |
else |
576 |
bname = argv[0]; |
577 |
argc--, argv++; |
578 |
|
579 |
if (!strcmp(bname, "gunzip")) |
580 |
uncompr = 1; |
581 |
else if (!strcmp(bname, "zcat")) |
582 |
copyout = uncompr = 1; |
583 |
|
584 |
while (argc > 0) { |
585 |
if (strcmp(*argv, "-c") == 0) |
586 |
copyout = 1; |
587 |
else if (strcmp(*argv, "-d") == 0) |
588 |
uncompr = 1; |
589 |
else if (strcmp(*argv, "-f") == 0) |
590 |
outmode[3] = 'f'; |
591 |
else if (strcmp(*argv, "-h") == 0) |
592 |
outmode[3] = 'h'; |
593 |
else if (strcmp(*argv, "-r") == 0) |
594 |
outmode[3] = 'R'; |
595 |
else if ((*argv)[0] == '-' && (*argv)[1] >= '1' && (*argv)[1] <= '9' && |
596 |
(*argv)[2] == 0) |
597 |
outmode[2] = (*argv)[1]; |
598 |
else |
599 |
break; |
600 |
argc--, argv++; |
601 |
} |
602 |
if (outmode[3] == ' ') |
603 |
outmode[3] = 0; |
604 |
if (argc == 0) { |
605 |
SET_BINARY_MODE(stdin); |
606 |
SET_BINARY_MODE(stdout); |
607 |
if (uncompr) { |
608 |
file = gzdopen(fileno(stdin), "rb"); |
609 |
if (file == NULL) error("can't gzdopen stdin"); |
610 |
gz_uncompress(file, stdout); |
611 |
} else { |
612 |
file = gzdopen(fileno(stdout), outmode); |
613 |
if (file == NULL) error("can't gzdopen stdout"); |
614 |
gz_compress(stdin, file); |
615 |
} |
616 |
} else { |
617 |
if (copyout) { |
618 |
SET_BINARY_MODE(stdout); |
619 |
} |
620 |
do { |
621 |
if (uncompr) { |
622 |
if (copyout) { |
623 |
file = gzopen(*argv, "rb"); |
624 |
if (file == NULL) |
625 |
fprintf(stderr, "%s: can't gzopen %s\n", prog, *argv); |
626 |
else |
627 |
gz_uncompress(file, stdout); |
628 |
} else { |
629 |
file_uncompress(*argv); |
630 |
} |
631 |
} else { |
632 |
if (copyout) { |
633 |
FILE * in = fopen(*argv, "rb"); |
634 |
|
635 |
if (in == NULL) { |
636 |
perror(*argv); |
637 |
} else { |
638 |
file = gzdopen(fileno(stdout), outmode); |
639 |
if (file == NULL) error("can't gzdopen stdout"); |
640 |
|
641 |
gz_compress(in, file); |
642 |
} |
643 |
|
644 |
} else { |
645 |
file_compress(*argv, outmode); |
646 |
} |
647 |
} |
648 |
} while (argv++, --argc); |
649 |
} |
650 |
return 0; |
651 |
} |