Chris@43
|
1 /* gzjoin -- command to join gzip files into one gzip file
|
Chris@43
|
2
|
Chris@43
|
3 Copyright (C) 2004, 2005, 2012 Mark Adler, all rights reserved
|
Chris@43
|
4 version 1.2, 14 Aug 2012
|
Chris@43
|
5
|
Chris@43
|
6 This software is provided 'as-is', without any express or implied
|
Chris@43
|
7 warranty. In no event will the author be held liable for any damages
|
Chris@43
|
8 arising from the use of this software.
|
Chris@43
|
9
|
Chris@43
|
10 Permission is granted to anyone to use this software for any purpose,
|
Chris@43
|
11 including commercial applications, and to alter it and redistribute it
|
Chris@43
|
12 freely, subject to the following restrictions:
|
Chris@43
|
13
|
Chris@43
|
14 1. The origin of this software must not be misrepresented; you must not
|
Chris@43
|
15 claim that you wrote the original software. If you use this software
|
Chris@43
|
16 in a product, an acknowledgment in the product documentation would be
|
Chris@43
|
17 appreciated but is not required.
|
Chris@43
|
18 2. Altered source versions must be plainly marked as such, and must not be
|
Chris@43
|
19 misrepresented as being the original software.
|
Chris@43
|
20 3. This notice may not be removed or altered from any source distribution.
|
Chris@43
|
21
|
Chris@43
|
22 Mark Adler madler@alumni.caltech.edu
|
Chris@43
|
23 */
|
Chris@43
|
24
|
Chris@43
|
25 /*
|
Chris@43
|
26 * Change history:
|
Chris@43
|
27 *
|
Chris@43
|
28 * 1.0 11 Dec 2004 - First version
|
Chris@43
|
29 * 1.1 12 Jun 2005 - Changed ssize_t to long for portability
|
Chris@43
|
30 * 1.2 14 Aug 2012 - Clean up for z_const usage
|
Chris@43
|
31 */
|
Chris@43
|
32
|
Chris@43
|
33 /*
|
Chris@43
|
34 gzjoin takes one or more gzip files on the command line and writes out a
|
Chris@43
|
35 single gzip file that will uncompress to the concatenation of the
|
Chris@43
|
36 uncompressed data from the individual gzip files. gzjoin does this without
|
Chris@43
|
37 having to recompress any of the data and without having to calculate a new
|
Chris@43
|
38 crc32 for the concatenated uncompressed data. gzjoin does however have to
|
Chris@43
|
39 decompress all of the input data in order to find the bits in the compressed
|
Chris@43
|
40 data that need to be modified to concatenate the streams.
|
Chris@43
|
41
|
Chris@43
|
42 gzjoin does not do an integrity check on the input gzip files other than
|
Chris@43
|
43 checking the gzip header and decompressing the compressed data. They are
|
Chris@43
|
44 otherwise assumed to be complete and correct.
|
Chris@43
|
45
|
Chris@43
|
46 Each joint between gzip files removes at least 18 bytes of previous trailer
|
Chris@43
|
47 and subsequent header, and inserts an average of about three bytes to the
|
Chris@43
|
48 compressed data in order to connect the streams. The output gzip file
|
Chris@43
|
49 has a minimal ten-byte gzip header with no file name or modification time.
|
Chris@43
|
50
|
Chris@43
|
51 This program was written to illustrate the use of the Z_BLOCK option of
|
Chris@43
|
52 inflate() and the crc32_combine() function. gzjoin will not compile with
|
Chris@43
|
53 versions of zlib earlier than 1.2.3.
|
Chris@43
|
54 */
|
Chris@43
|
55
|
Chris@43
|
56 #include <stdio.h> /* fputs(), fprintf(), fwrite(), putc() */
|
Chris@43
|
57 #include <stdlib.h> /* exit(), malloc(), free() */
|
Chris@43
|
58 #include <fcntl.h> /* open() */
|
Chris@43
|
59 #include <unistd.h> /* close(), read(), lseek() */
|
Chris@43
|
60 #include "zlib.h"
|
Chris@43
|
61 /* crc32(), crc32_combine(), inflateInit2(), inflate(), inflateEnd() */
|
Chris@43
|
62
|
Chris@43
|
63 #define local static
|
Chris@43
|
64
|
Chris@43
|
65 /* exit with an error (return a value to allow use in an expression) */
|
Chris@43
|
66 local int bail(char *why1, char *why2)
|
Chris@43
|
67 {
|
Chris@43
|
68 fprintf(stderr, "gzjoin error: %s%s, output incomplete\n", why1, why2);
|
Chris@43
|
69 exit(1);
|
Chris@43
|
70 return 0;
|
Chris@43
|
71 }
|
Chris@43
|
72
|
Chris@43
|
73 /* -- simple buffered file input with access to the buffer -- */
|
Chris@43
|
74
|
Chris@43
|
75 #define CHUNK 32768 /* must be a power of two and fit in unsigned */
|
Chris@43
|
76
|
Chris@43
|
77 /* bin buffered input file type */
|
Chris@43
|
78 typedef struct {
|
Chris@43
|
79 char *name; /* name of file for error messages */
|
Chris@43
|
80 int fd; /* file descriptor */
|
Chris@43
|
81 unsigned left; /* bytes remaining at next */
|
Chris@43
|
82 unsigned char *next; /* next byte to read */
|
Chris@43
|
83 unsigned char *buf; /* allocated buffer of length CHUNK */
|
Chris@43
|
84 } bin;
|
Chris@43
|
85
|
Chris@43
|
86 /* close a buffered file and free allocated memory */
|
Chris@43
|
87 local void bclose(bin *in)
|
Chris@43
|
88 {
|
Chris@43
|
89 if (in != NULL) {
|
Chris@43
|
90 if (in->fd != -1)
|
Chris@43
|
91 close(in->fd);
|
Chris@43
|
92 if (in->buf != NULL)
|
Chris@43
|
93 free(in->buf);
|
Chris@43
|
94 free(in);
|
Chris@43
|
95 }
|
Chris@43
|
96 }
|
Chris@43
|
97
|
Chris@43
|
98 /* open a buffered file for input, return a pointer to type bin, or NULL on
|
Chris@43
|
99 failure */
|
Chris@43
|
100 local bin *bopen(char *name)
|
Chris@43
|
101 {
|
Chris@43
|
102 bin *in;
|
Chris@43
|
103
|
Chris@43
|
104 in = malloc(sizeof(bin));
|
Chris@43
|
105 if (in == NULL)
|
Chris@43
|
106 return NULL;
|
Chris@43
|
107 in->buf = malloc(CHUNK);
|
Chris@43
|
108 in->fd = open(name, O_RDONLY, 0);
|
Chris@43
|
109 if (in->buf == NULL || in->fd == -1) {
|
Chris@43
|
110 bclose(in);
|
Chris@43
|
111 return NULL;
|
Chris@43
|
112 }
|
Chris@43
|
113 in->left = 0;
|
Chris@43
|
114 in->next = in->buf;
|
Chris@43
|
115 in->name = name;
|
Chris@43
|
116 return in;
|
Chris@43
|
117 }
|
Chris@43
|
118
|
Chris@43
|
119 /* load buffer from file, return -1 on read error, 0 or 1 on success, with
|
Chris@43
|
120 1 indicating that end-of-file was reached */
|
Chris@43
|
121 local int bload(bin *in)
|
Chris@43
|
122 {
|
Chris@43
|
123 long len;
|
Chris@43
|
124
|
Chris@43
|
125 if (in == NULL)
|
Chris@43
|
126 return -1;
|
Chris@43
|
127 if (in->left != 0)
|
Chris@43
|
128 return 0;
|
Chris@43
|
129 in->next = in->buf;
|
Chris@43
|
130 do {
|
Chris@43
|
131 len = (long)read(in->fd, in->buf + in->left, CHUNK - in->left);
|
Chris@43
|
132 if (len < 0)
|
Chris@43
|
133 return -1;
|
Chris@43
|
134 in->left += (unsigned)len;
|
Chris@43
|
135 } while (len != 0 && in->left < CHUNK);
|
Chris@43
|
136 return len == 0 ? 1 : 0;
|
Chris@43
|
137 }
|
Chris@43
|
138
|
Chris@43
|
139 /* get a byte from the file, bail if end of file */
|
Chris@43
|
140 #define bget(in) (in->left ? 0 : bload(in), \
|
Chris@43
|
141 in->left ? (in->left--, *(in->next)++) : \
|
Chris@43
|
142 bail("unexpected end of file on ", in->name))
|
Chris@43
|
143
|
Chris@43
|
144 /* get a four-byte little-endian unsigned integer from file */
|
Chris@43
|
145 local unsigned long bget4(bin *in)
|
Chris@43
|
146 {
|
Chris@43
|
147 unsigned long val;
|
Chris@43
|
148
|
Chris@43
|
149 val = bget(in);
|
Chris@43
|
150 val += (unsigned long)(bget(in)) << 8;
|
Chris@43
|
151 val += (unsigned long)(bget(in)) << 16;
|
Chris@43
|
152 val += (unsigned long)(bget(in)) << 24;
|
Chris@43
|
153 return val;
|
Chris@43
|
154 }
|
Chris@43
|
155
|
Chris@43
|
156 /* skip bytes in file */
|
Chris@43
|
157 local void bskip(bin *in, unsigned skip)
|
Chris@43
|
158 {
|
Chris@43
|
159 /* check pointer */
|
Chris@43
|
160 if (in == NULL)
|
Chris@43
|
161 return;
|
Chris@43
|
162
|
Chris@43
|
163 /* easy case -- skip bytes in buffer */
|
Chris@43
|
164 if (skip <= in->left) {
|
Chris@43
|
165 in->left -= skip;
|
Chris@43
|
166 in->next += skip;
|
Chris@43
|
167 return;
|
Chris@43
|
168 }
|
Chris@43
|
169
|
Chris@43
|
170 /* skip what's in buffer, discard buffer contents */
|
Chris@43
|
171 skip -= in->left;
|
Chris@43
|
172 in->left = 0;
|
Chris@43
|
173
|
Chris@43
|
174 /* seek past multiples of CHUNK bytes */
|
Chris@43
|
175 if (skip > CHUNK) {
|
Chris@43
|
176 unsigned left;
|
Chris@43
|
177
|
Chris@43
|
178 left = skip & (CHUNK - 1);
|
Chris@43
|
179 if (left == 0) {
|
Chris@43
|
180 /* exact number of chunks: seek all the way minus one byte to check
|
Chris@43
|
181 for end-of-file with a read */
|
Chris@43
|
182 lseek(in->fd, skip - 1, SEEK_CUR);
|
Chris@43
|
183 if (read(in->fd, in->buf, 1) != 1)
|
Chris@43
|
184 bail("unexpected end of file on ", in->name);
|
Chris@43
|
185 return;
|
Chris@43
|
186 }
|
Chris@43
|
187
|
Chris@43
|
188 /* skip the integral chunks, update skip with remainder */
|
Chris@43
|
189 lseek(in->fd, skip - left, SEEK_CUR);
|
Chris@43
|
190 skip = left;
|
Chris@43
|
191 }
|
Chris@43
|
192
|
Chris@43
|
193 /* read more input and skip remainder */
|
Chris@43
|
194 bload(in);
|
Chris@43
|
195 if (skip > in->left)
|
Chris@43
|
196 bail("unexpected end of file on ", in->name);
|
Chris@43
|
197 in->left -= skip;
|
Chris@43
|
198 in->next += skip;
|
Chris@43
|
199 }
|
Chris@43
|
200
|
Chris@43
|
201 /* -- end of buffered input functions -- */
|
Chris@43
|
202
|
Chris@43
|
203 /* skip the gzip header from file in */
|
Chris@43
|
204 local void gzhead(bin *in)
|
Chris@43
|
205 {
|
Chris@43
|
206 int flags;
|
Chris@43
|
207
|
Chris@43
|
208 /* verify gzip magic header and compression method */
|
Chris@43
|
209 if (bget(in) != 0x1f || bget(in) != 0x8b || bget(in) != 8)
|
Chris@43
|
210 bail(in->name, " is not a valid gzip file");
|
Chris@43
|
211
|
Chris@43
|
212 /* get and verify flags */
|
Chris@43
|
213 flags = bget(in);
|
Chris@43
|
214 if ((flags & 0xe0) != 0)
|
Chris@43
|
215 bail("unknown reserved bits set in ", in->name);
|
Chris@43
|
216
|
Chris@43
|
217 /* skip modification time, extra flags, and os */
|
Chris@43
|
218 bskip(in, 6);
|
Chris@43
|
219
|
Chris@43
|
220 /* skip extra field if present */
|
Chris@43
|
221 if (flags & 4) {
|
Chris@43
|
222 unsigned len;
|
Chris@43
|
223
|
Chris@43
|
224 len = bget(in);
|
Chris@43
|
225 len += (unsigned)(bget(in)) << 8;
|
Chris@43
|
226 bskip(in, len);
|
Chris@43
|
227 }
|
Chris@43
|
228
|
Chris@43
|
229 /* skip file name if present */
|
Chris@43
|
230 if (flags & 8)
|
Chris@43
|
231 while (bget(in) != 0)
|
Chris@43
|
232 ;
|
Chris@43
|
233
|
Chris@43
|
234 /* skip comment if present */
|
Chris@43
|
235 if (flags & 16)
|
Chris@43
|
236 while (bget(in) != 0)
|
Chris@43
|
237 ;
|
Chris@43
|
238
|
Chris@43
|
239 /* skip header crc if present */
|
Chris@43
|
240 if (flags & 2)
|
Chris@43
|
241 bskip(in, 2);
|
Chris@43
|
242 }
|
Chris@43
|
243
|
Chris@43
|
244 /* write a four-byte little-endian unsigned integer to out */
|
Chris@43
|
245 local void put4(unsigned long val, FILE *out)
|
Chris@43
|
246 {
|
Chris@43
|
247 putc(val & 0xff, out);
|
Chris@43
|
248 putc((val >> 8) & 0xff, out);
|
Chris@43
|
249 putc((val >> 16) & 0xff, out);
|
Chris@43
|
250 putc((val >> 24) & 0xff, out);
|
Chris@43
|
251 }
|
Chris@43
|
252
|
Chris@43
|
253 /* Load up zlib stream from buffered input, bail if end of file */
|
Chris@43
|
254 local void zpull(z_streamp strm, bin *in)
|
Chris@43
|
255 {
|
Chris@43
|
256 if (in->left == 0)
|
Chris@43
|
257 bload(in);
|
Chris@43
|
258 if (in->left == 0)
|
Chris@43
|
259 bail("unexpected end of file on ", in->name);
|
Chris@43
|
260 strm->avail_in = in->left;
|
Chris@43
|
261 strm->next_in = in->next;
|
Chris@43
|
262 }
|
Chris@43
|
263
|
Chris@43
|
264 /* Write header for gzip file to out and initialize trailer. */
|
Chris@43
|
265 local void gzinit(unsigned long *crc, unsigned long *tot, FILE *out)
|
Chris@43
|
266 {
|
Chris@43
|
267 fwrite("\x1f\x8b\x08\0\0\0\0\0\0\xff", 1, 10, out);
|
Chris@43
|
268 *crc = crc32(0L, Z_NULL, 0);
|
Chris@43
|
269 *tot = 0;
|
Chris@43
|
270 }
|
Chris@43
|
271
|
Chris@43
|
272 /* Copy the compressed data from name, zeroing the last block bit of the last
|
Chris@43
|
273 block if clr is true, and adding empty blocks as needed to get to a byte
|
Chris@43
|
274 boundary. If clr is false, then the last block becomes the last block of
|
Chris@43
|
275 the output, and the gzip trailer is written. crc and tot maintains the
|
Chris@43
|
276 crc and length (modulo 2^32) of the output for the trailer. The resulting
|
Chris@43
|
277 gzip file is written to out. gzinit() must be called before the first call
|
Chris@43
|
278 of gzcopy() to write the gzip header and to initialize crc and tot. */
|
Chris@43
|
279 local void gzcopy(char *name, int clr, unsigned long *crc, unsigned long *tot,
|
Chris@43
|
280 FILE *out)
|
Chris@43
|
281 {
|
Chris@43
|
282 int ret; /* return value from zlib functions */
|
Chris@43
|
283 int pos; /* where the "last block" bit is in byte */
|
Chris@43
|
284 int last; /* true if processing the last block */
|
Chris@43
|
285 bin *in; /* buffered input file */
|
Chris@43
|
286 unsigned char *start; /* start of compressed data in buffer */
|
Chris@43
|
287 unsigned char *junk; /* buffer for uncompressed data -- discarded */
|
Chris@43
|
288 z_off_t len; /* length of uncompressed data (support > 4 GB) */
|
Chris@43
|
289 z_stream strm; /* zlib inflate stream */
|
Chris@43
|
290
|
Chris@43
|
291 /* open gzip file and skip header */
|
Chris@43
|
292 in = bopen(name);
|
Chris@43
|
293 if (in == NULL)
|
Chris@43
|
294 bail("could not open ", name);
|
Chris@43
|
295 gzhead(in);
|
Chris@43
|
296
|
Chris@43
|
297 /* allocate buffer for uncompressed data and initialize raw inflate
|
Chris@43
|
298 stream */
|
Chris@43
|
299 junk = malloc(CHUNK);
|
Chris@43
|
300 strm.zalloc = Z_NULL;
|
Chris@43
|
301 strm.zfree = Z_NULL;
|
Chris@43
|
302 strm.opaque = Z_NULL;
|
Chris@43
|
303 strm.avail_in = 0;
|
Chris@43
|
304 strm.next_in = Z_NULL;
|
Chris@43
|
305 ret = inflateInit2(&strm, -15);
|
Chris@43
|
306 if (junk == NULL || ret != Z_OK)
|
Chris@43
|
307 bail("out of memory", "");
|
Chris@43
|
308
|
Chris@43
|
309 /* inflate and copy compressed data, clear last-block bit if requested */
|
Chris@43
|
310 len = 0;
|
Chris@43
|
311 zpull(&strm, in);
|
Chris@43
|
312 start = in->next;
|
Chris@43
|
313 last = start[0] & 1;
|
Chris@43
|
314 if (last && clr)
|
Chris@43
|
315 start[0] &= ~1;
|
Chris@43
|
316 strm.avail_out = 0;
|
Chris@43
|
317 for (;;) {
|
Chris@43
|
318 /* if input used and output done, write used input and get more */
|
Chris@43
|
319 if (strm.avail_in == 0 && strm.avail_out != 0) {
|
Chris@43
|
320 fwrite(start, 1, strm.next_in - start, out);
|
Chris@43
|
321 start = in->buf;
|
Chris@43
|
322 in->left = 0;
|
Chris@43
|
323 zpull(&strm, in);
|
Chris@43
|
324 }
|
Chris@43
|
325
|
Chris@43
|
326 /* decompress -- return early when end-of-block reached */
|
Chris@43
|
327 strm.avail_out = CHUNK;
|
Chris@43
|
328 strm.next_out = junk;
|
Chris@43
|
329 ret = inflate(&strm, Z_BLOCK);
|
Chris@43
|
330 switch (ret) {
|
Chris@43
|
331 case Z_MEM_ERROR:
|
Chris@43
|
332 bail("out of memory", "");
|
Chris@43
|
333 case Z_DATA_ERROR:
|
Chris@43
|
334 bail("invalid compressed data in ", in->name);
|
Chris@43
|
335 }
|
Chris@43
|
336
|
Chris@43
|
337 /* update length of uncompressed data */
|
Chris@43
|
338 len += CHUNK - strm.avail_out;
|
Chris@43
|
339
|
Chris@43
|
340 /* check for block boundary (only get this when block copied out) */
|
Chris@43
|
341 if (strm.data_type & 128) {
|
Chris@43
|
342 /* if that was the last block, then done */
|
Chris@43
|
343 if (last)
|
Chris@43
|
344 break;
|
Chris@43
|
345
|
Chris@43
|
346 /* number of unused bits in last byte */
|
Chris@43
|
347 pos = strm.data_type & 7;
|
Chris@43
|
348
|
Chris@43
|
349 /* find the next last-block bit */
|
Chris@43
|
350 if (pos != 0) {
|
Chris@43
|
351 /* next last-block bit is in last used byte */
|
Chris@43
|
352 pos = 0x100 >> pos;
|
Chris@43
|
353 last = strm.next_in[-1] & pos;
|
Chris@43
|
354 if (last && clr)
|
Chris@43
|
355 in->buf[strm.next_in - in->buf - 1] &= ~pos;
|
Chris@43
|
356 }
|
Chris@43
|
357 else {
|
Chris@43
|
358 /* next last-block bit is in next unused byte */
|
Chris@43
|
359 if (strm.avail_in == 0) {
|
Chris@43
|
360 /* don't have that byte yet -- get it */
|
Chris@43
|
361 fwrite(start, 1, strm.next_in - start, out);
|
Chris@43
|
362 start = in->buf;
|
Chris@43
|
363 in->left = 0;
|
Chris@43
|
364 zpull(&strm, in);
|
Chris@43
|
365 }
|
Chris@43
|
366 last = strm.next_in[0] & 1;
|
Chris@43
|
367 if (last && clr)
|
Chris@43
|
368 in->buf[strm.next_in - in->buf] &= ~1;
|
Chris@43
|
369 }
|
Chris@43
|
370 }
|
Chris@43
|
371 }
|
Chris@43
|
372
|
Chris@43
|
373 /* update buffer with unused input */
|
Chris@43
|
374 in->left = strm.avail_in;
|
Chris@43
|
375 in->next = in->buf + (strm.next_in - in->buf);
|
Chris@43
|
376
|
Chris@43
|
377 /* copy used input, write empty blocks to get to byte boundary */
|
Chris@43
|
378 pos = strm.data_type & 7;
|
Chris@43
|
379 fwrite(start, 1, in->next - start - 1, out);
|
Chris@43
|
380 last = in->next[-1];
|
Chris@43
|
381 if (pos == 0 || !clr)
|
Chris@43
|
382 /* already at byte boundary, or last file: write last byte */
|
Chris@43
|
383 putc(last, out);
|
Chris@43
|
384 else {
|
Chris@43
|
385 /* append empty blocks to last byte */
|
Chris@43
|
386 last &= ((0x100 >> pos) - 1); /* assure unused bits are zero */
|
Chris@43
|
387 if (pos & 1) {
|
Chris@43
|
388 /* odd -- append an empty stored block */
|
Chris@43
|
389 putc(last, out);
|
Chris@43
|
390 if (pos == 1)
|
Chris@43
|
391 putc(0, out); /* two more bits in block header */
|
Chris@43
|
392 fwrite("\0\0\xff\xff", 1, 4, out);
|
Chris@43
|
393 }
|
Chris@43
|
394 else {
|
Chris@43
|
395 /* even -- append 1, 2, or 3 empty fixed blocks */
|
Chris@43
|
396 switch (pos) {
|
Chris@43
|
397 case 6:
|
Chris@43
|
398 putc(last | 8, out);
|
Chris@43
|
399 last = 0;
|
Chris@43
|
400 case 4:
|
Chris@43
|
401 putc(last | 0x20, out);
|
Chris@43
|
402 last = 0;
|
Chris@43
|
403 case 2:
|
Chris@43
|
404 putc(last | 0x80, out);
|
Chris@43
|
405 putc(0, out);
|
Chris@43
|
406 }
|
Chris@43
|
407 }
|
Chris@43
|
408 }
|
Chris@43
|
409
|
Chris@43
|
410 /* update crc and tot */
|
Chris@43
|
411 *crc = crc32_combine(*crc, bget4(in), len);
|
Chris@43
|
412 *tot += (unsigned long)len;
|
Chris@43
|
413
|
Chris@43
|
414 /* clean up */
|
Chris@43
|
415 inflateEnd(&strm);
|
Chris@43
|
416 free(junk);
|
Chris@43
|
417 bclose(in);
|
Chris@43
|
418
|
Chris@43
|
419 /* write trailer if this is the last gzip file */
|
Chris@43
|
420 if (!clr) {
|
Chris@43
|
421 put4(*crc, out);
|
Chris@43
|
422 put4(*tot, out);
|
Chris@43
|
423 }
|
Chris@43
|
424 }
|
Chris@43
|
425
|
Chris@43
|
426 /* join the gzip files on the command line, write result to stdout */
|
Chris@43
|
427 int main(int argc, char **argv)
|
Chris@43
|
428 {
|
Chris@43
|
429 unsigned long crc, tot; /* running crc and total uncompressed length */
|
Chris@43
|
430
|
Chris@43
|
431 /* skip command name */
|
Chris@43
|
432 argc--;
|
Chris@43
|
433 argv++;
|
Chris@43
|
434
|
Chris@43
|
435 /* show usage if no arguments */
|
Chris@43
|
436 if (argc == 0) {
|
Chris@43
|
437 fputs("gzjoin usage: gzjoin f1.gz [f2.gz [f3.gz ...]] > fjoin.gz\n",
|
Chris@43
|
438 stderr);
|
Chris@43
|
439 return 0;
|
Chris@43
|
440 }
|
Chris@43
|
441
|
Chris@43
|
442 /* join gzip files on command line and write to stdout */
|
Chris@43
|
443 gzinit(&crc, &tot, stdout);
|
Chris@43
|
444 while (argc--)
|
Chris@43
|
445 gzcopy(*argv++, argc, &crc, &tot, stdout);
|
Chris@43
|
446
|
Chris@43
|
447 /* done */
|
Chris@43
|
448 return 0;
|
Chris@43
|
449 }
|