219 |
mateuszvis |
1 |
/*
|
|
|
2 |
* This file is part of the FDNPKG project
|
|
|
3 |
* http://fdnpkg.sourceforge.net
|
|
|
4 |
*
|
|
|
5 |
* Copyright (C) 2012-2016 Mateusz Viste. All rights reserved.
|
|
|
6 |
*
|
|
|
7 |
* Simple library providing functions to unzip files from zip archives.
|
|
|
8 |
*/
|
|
|
9 |
|
|
|
10 |
#include <stdio.h> /* printf(), FILE, fclose()... */
|
|
|
11 |
#include <stdlib.h> /* NULL */
|
|
|
12 |
#include <string.h> /* memset() */
|
|
|
13 |
#include <time.h> /* mktime() */
|
|
|
14 |
#include <utime.h> /* utime() */
|
|
|
15 |
#include <unistd.h> /* unlink() */
|
|
|
16 |
|
|
|
17 |
#include "crc32.h"
|
|
|
18 |
#include "kprintf.h"
|
|
|
19 |
#include "parsecmd.h"
|
|
|
20 |
#ifndef NOLZMA
|
|
|
21 |
#include "lzmadec.h" /* LZMA support */
|
|
|
22 |
#endif
|
|
|
23 |
#include "inf.h" /* DEFLATE support */
|
|
|
24 |
#include "version.h"
|
|
|
25 |
|
|
|
26 |
#include "libunzip.h" /* include self for control */
|
|
|
27 |
|
|
|
28 |
|
|
|
29 |
/* converts a "DOS format" timestamp into unix timestamp. The DOS timestamp is constructed an array of 4 bytes, that contains following data at the bit level:
|
|
|
30 |
* HHHHHMMM MMMSSSSS YYYYYYYM MMMDDDDD
|
|
|
31 |
* where:
|
|
|
32 |
* day of month is always within 1-31 range;
|
|
|
33 |
* month is always within 1-12 range;
|
|
|
34 |
* year starts from 1980 and continues for 127 years
|
|
|
35 |
* seconds are actually not 0-59 but rather 0-29 as there are only 32 possible values – to get actual seconds multiply this field by 2;
|
|
|
36 |
* minutes are always within 0-59 range;
|
|
|
37 |
* hours are always within 0-23 range. */
|
|
|
38 |
static time_t dostime2unix(unsigned char *buff) {
|
|
|
39 |
struct tm curtime;
|
|
|
40 |
time_t result;
|
|
|
41 |
memset(&curtime, 0, sizeof(curtime)); /* make sure to set everything in curtime to 0's */
|
|
|
42 |
curtime.tm_sec = (buff[0] & 31) << 1; /* seconds (0..60) */
|
|
|
43 |
curtime.tm_min = (((buff[1] << 8) | buff[0]) >> 5) & 63 ; /* minutes after the hour (0..59) */
|
|
|
44 |
curtime.tm_hour = (buff[1] >> 3); /* hours since midnight (0..23) */
|
|
|
45 |
curtime.tm_mday = buff[2] & 31; /* day of the month (1..31) */
|
|
|
46 |
curtime.tm_mon = ((((buff[3] << 8) | buff[2]) >> 5) & 15) - 1; /* months since January (0, 11) */
|
|
|
47 |
curtime.tm_year = (buff[3] >> 1) + 80; /* years since 1900 */
|
|
|
48 |
curtime.tm_wday = 0; /* days since Sunday (0..6) - leave 0, mktime() will set it */
|
|
|
49 |
curtime.tm_yday = 0; /* days since January 1 (0..365]) - leave 0, mktime() will set it */
|
|
|
50 |
curtime.tm_isdst = -1; /* Daylight Saving Time flag. Positive if DST is in effect, zero if not and negative if no information is available */
|
|
|
51 |
result = mktime(&curtime);
|
|
|
52 |
if (result == (time_t)-1) return(0);
|
|
|
53 |
return(result);
|
|
|
54 |
}
|
|
|
55 |
|
|
|
56 |
|
|
|
57 |
#ifndef NOLZMA
|
|
|
58 |
/* this is a wrapper on malloc(), used as a callback by lzmadec */
|
|
|
59 |
static void *SzAlloc(void *p, size_t size) {
|
|
|
60 |
p = p; /* for gcc to not complain */
|
|
|
61 |
if (size == 0) return(0);
|
|
|
62 |
return(malloc(size));
|
|
|
63 |
}
|
|
|
64 |
|
|
|
65 |
/* this is a wrapper on free(), used as a callback by lzmadec */
|
|
|
66 |
static void SzFree(void *p, void *address) {
|
|
|
67 |
p = p; /* for gcc to not complain */
|
|
|
68 |
free(address);
|
|
|
69 |
}
|
|
|
70 |
#endif
|
|
|
71 |
|
|
|
72 |
|
|
|
73 |
|
|
|
74 |
/* opens a zip file and provides the list of files in the archive.
|
|
|
75 |
returns a pointer to a ziplist (linked list) with all records, or NULL on error.
|
|
|
76 |
The ziplist is allocated automatically, and must be freed via zip_freelist. */
|
|
|
77 |
struct ziplist *zip_listfiles(FILE *fd) {
|
|
|
78 |
struct ziplist *reslist = NULL;
|
|
|
79 |
struct ziplist *newentry;
|
|
|
80 |
unsigned long entrysig;
|
|
|
81 |
unsigned short filenamelen, extrafieldlen, filecommentlen;
|
|
|
82 |
unsigned long compfilelen;
|
|
|
83 |
int centraldirectoryfound = 0;
|
|
|
84 |
unsigned int ux;
|
|
|
85 |
unsigned char hdrbuff[64];
|
|
|
86 |
|
|
|
87 |
rewind(fd); /* make sure the file cursor is at the very beginning of the file */
|
|
|
88 |
|
|
|
89 |
for (;;) { /* read entry after entry */
|
|
|
90 |
int x, eofflag;
|
|
|
91 |
long longbuff;
|
|
|
92 |
entrysig = 0;
|
|
|
93 |
eofflag = 0;
|
|
|
94 |
/* read the entry signature first */
|
|
|
95 |
for (x = 0; x < 32; x += 8) {
|
|
|
96 |
if ((longbuff = fgetc(fd)) == EOF) {
|
|
|
97 |
eofflag = 1;
|
|
|
98 |
break;
|
|
|
99 |
}
|
|
|
100 |
entrysig |= (longbuff << x);
|
|
|
101 |
}
|
|
|
102 |
if (eofflag != 0) break;
|
|
|
103 |
/* printf("sig: 0x%08x\n", entrysig); */
|
|
|
104 |
if (entrysig == 0x04034b50ul) { /* local file */
|
|
|
105 |
unsigned int generalpurposeflags;
|
|
|
106 |
/* read and parse the zip header */
|
|
|
107 |
fread(hdrbuff, 1, 26, fd);
|
|
|
108 |
/* read filename's length so I can allocate the proper amound of mem */
|
|
|
109 |
filenamelen = hdrbuff[23];
|
|
|
110 |
filenamelen <<= 8;
|
|
|
111 |
filenamelen |= hdrbuff[22];
|
|
|
112 |
/* create new entry and link it into the list */
|
|
|
113 |
newentry = calloc(sizeof(struct ziplist) + filenamelen, 1);
|
|
|
114 |
if (newentry == NULL) {
|
|
|
115 |
kitten_puts(8, 0, "Out of memory!");
|
|
|
116 |
zip_freelist(&reslist);
|
|
|
117 |
break;
|
|
|
118 |
}
|
|
|
119 |
newentry->nextfile = reslist;
|
|
|
120 |
newentry->flags = 0;
|
|
|
121 |
reslist = newentry;
|
|
|
122 |
/* read further areas of the header, and fill zip entry */
|
|
|
123 |
generalpurposeflags = hdrbuff[3]; /* parse the general */
|
|
|
124 |
generalpurposeflags <<= 8; /* purpose flags and */
|
|
|
125 |
generalpurposeflags |= hdrbuff[2]; /* save them for later */
|
|
|
126 |
newentry->compmethod = hdrbuff[4] | (hdrbuff[5] << 8);
|
|
|
127 |
newentry->timestamp = dostime2unix(&hdrbuff[6]);
|
|
|
128 |
newentry->crc32 = 0;
|
|
|
129 |
for (x = 13; x >= 10; x--) {
|
|
|
130 |
newentry->crc32 <<= 8;
|
|
|
131 |
newentry->crc32 |= hdrbuff[x];
|
|
|
132 |
}
|
|
|
133 |
newentry->compressedfilelen = 0;
|
|
|
134 |
for (x = 17; x >= 14; x--) {
|
|
|
135 |
newentry->compressedfilelen <<= 8;
|
|
|
136 |
newentry->compressedfilelen |= hdrbuff[x];
|
|
|
137 |
}
|
|
|
138 |
newentry->filelen = 0;
|
|
|
139 |
for (x = 21; x >= 18; x--) {
|
|
|
140 |
newentry->filelen <<= 8;
|
|
|
141 |
newentry->filelen |= hdrbuff[x];
|
|
|
142 |
}
|
|
|
143 |
extrafieldlen = hdrbuff[25];
|
|
|
144 |
extrafieldlen <<= 8;
|
|
|
145 |
extrafieldlen |= hdrbuff[24];
|
|
|
146 |
/* printf("Filename len: %d / extrafield len: %d / compfile len: %ld / filelen: %ld\n", filenamelen, extrafieldlen, newentry->compressedfilelen, newentry->filelen); */
|
|
|
147 |
/* check general purpose flags */
|
|
|
148 |
if ((generalpurposeflags & 1) != 0) newentry->flags |= ZIP_FLAG_ENCRYPTED;
|
|
|
149 |
/* parse the filename */
|
|
|
150 |
for (ux = 0; ux < filenamelen; ux++) newentry->filename[ux] = fgetc(fd); /* store filename */
|
|
|
151 |
if (newentry->filename[filenamelen - 1] == '/') newentry->flags |= ZIP_FLAG_ISADIR; /* if filename ends with / it's a dir. Note that ZIP forbids the usage of '\' in ZIP paths anyway */
|
|
|
152 |
/* printf("Filename: %s (%ld bytes compressed)\n", newentry->filename, newentry->compressedfilelen); */
|
|
|
153 |
newentry->dataoffset = ftell(fd) + extrafieldlen;
|
|
|
154 |
/* skip rest of fields and data */
|
|
|
155 |
fseek(fd, (extrafieldlen + newentry->compressedfilelen), SEEK_CUR);
|
|
|
156 |
} else if (entrysig == 0x02014b50ul) { /* central directory */
|
|
|
157 |
centraldirectoryfound = 1;
|
|
|
158 |
/* parse header now */
|
|
|
159 |
fread(hdrbuff, 1, 42, fd);
|
|
|
160 |
filenamelen = hdrbuff[22] | (hdrbuff[23] << 8);
|
|
|
161 |
extrafieldlen = hdrbuff[24] | (hdrbuff[25] << 8);
|
|
|
162 |
filecommentlen = hdrbuff[26] | (hdrbuff[27] << 8);
|
|
|
163 |
compfilelen = 0;
|
|
|
164 |
for (x = 17; x >= 14; x--) {
|
|
|
165 |
compfilelen <<= 8;
|
|
|
166 |
compfilelen |= hdrbuff[x];
|
|
|
167 |
}
|
|
|
168 |
/* printf("central dir\n"); */
|
|
|
169 |
/* skip rest of fields and data */
|
|
|
170 |
fseek(fd, (filenamelen + extrafieldlen + compfilelen + filecommentlen), SEEK_CUR);
|
|
|
171 |
} else if (entrysig == 0x08074b50ul) { /* Data descriptor header */
|
|
|
172 |
/* no need to read the header we just have to skip it */
|
|
|
173 |
fseek(fd, 12, SEEK_CUR); /* the header is 3x4 bytes (CRC + compressed len + uncompressed len) */
|
|
|
174 |
} else { /* unknown sig */
|
|
|
175 |
kitten_printf(8, 1, "unknown zip sig: 0x%08lx", entrysig);
|
|
|
176 |
puts("");
|
|
|
177 |
zip_freelist(&reslist);
|
|
|
178 |
break;
|
|
|
179 |
}
|
|
|
180 |
}
|
|
|
181 |
/* if we got no central directory record, the file is incomplete */
|
|
|
182 |
if (centraldirectoryfound == 0) zip_freelist(&reslist);
|
|
|
183 |
return(reslist);
|
|
|
184 |
}
|
|
|
185 |
|
|
|
186 |
|
|
|
187 |
|
|
|
188 |
/* unzips a file. zipfd points to the open zip file, curzipnode to the entry to extract, and fulldestfilename is the destination file where to unzip it. returns 0 on success, non-zero otherwise. */
|
|
|
189 |
int zip_unzip(FILE *zipfd, struct ziplist *curzipnode, char *fulldestfilename) {
|
|
|
190 |
#define buffsize 32 * 1024l /* MUST be at least 32K */
|
|
|
191 |
FILE *filefd;
|
|
|
192 |
unsigned long cksum;
|
|
|
193 |
int extract_res;
|
|
|
194 |
unsigned char *buff;
|
|
|
195 |
struct utimbuf filetimestamp;
|
|
|
196 |
|
|
|
197 |
/* first of all, check we support the compression method */
|
|
|
198 |
switch (curzipnode->compmethod) {
|
|
|
199 |
case 0: /* stored */
|
|
|
200 |
case 8: /* deflated */
|
|
|
201 |
#ifndef NOLZMA
|
|
|
202 |
case 14: /* lzma */
|
|
|
203 |
#endif
|
|
|
204 |
break;
|
|
|
205 |
default: /* unsupported compression method, sorry */
|
|
|
206 |
return(-1);
|
|
|
207 |
break;
|
|
|
208 |
}
|
|
|
209 |
|
|
|
210 |
/* open the dst file */
|
|
|
211 |
filefd = fopen(fulldestfilename, "wb");
|
|
|
212 |
if (filefd == NULL) return(-2); /* failed to open the dst file */
|
|
|
213 |
|
|
|
214 |
/* allocate buffers for data I/O */
|
|
|
215 |
buff = malloc(buffsize);
|
|
|
216 |
if (buff == NULL) {
|
|
|
217 |
fclose(filefd);
|
|
|
218 |
unlink(fulldestfilename); /* remove the failed file once it is closed */
|
|
|
219 |
return(-6);
|
|
|
220 |
}
|
|
|
221 |
|
|
|
222 |
if (fseek(zipfd, curzipnode->dataoffset, SEEK_SET) != 0) { /* set the reading position inside the zip file */
|
|
|
223 |
free(buff);
|
|
|
224 |
fclose(filefd);
|
|
|
225 |
unlink(fulldestfilename); /* remove the failed file once it is closed */
|
|
|
226 |
return(-7);
|
|
|
227 |
}
|
|
|
228 |
extract_res = -255;
|
|
|
229 |
|
|
|
230 |
cksum = crc32_init(); /* init the crc32 */
|
|
|
231 |
|
|
|
232 |
if (curzipnode->compmethod == 0) { /* if the file is stored, copy it over */
|
|
|
233 |
long i, toread;
|
|
|
234 |
extract_res = 0; /* assume we will succeed */
|
|
|
235 |
for (i = 0; i < curzipnode->filelen;) {
|
|
|
236 |
toread = curzipnode->filelen - i;
|
|
|
237 |
if (toread > buffsize) toread = buffsize;
|
|
|
238 |
if (fread(buff, toread, 1, zipfd) != 1) extract_res = -3; /* read a chunk of data */
|
|
|
239 |
crc32_feed(&cksum, buff, toread); /* update the crc32 checksum */
|
|
|
240 |
if (fwrite(buff, toread, 1, filefd) != 1) extract_res = -4; /* write data chunk to dst file */
|
|
|
241 |
i += toread;
|
|
|
242 |
}
|
|
|
243 |
} else if (curzipnode->compmethod == 8) { /* if the file is deflated, inflate it */
|
|
|
244 |
extract_res = inf(zipfd, filefd, buff, &cksum, curzipnode->compressedfilelen);
|
|
|
245 |
#ifndef NOLZMA
|
|
|
246 |
} else if (curzipnode->compmethod == 14) { /* LZMA */
|
|
|
247 |
#define lzmaoutbufflen 32768u
|
|
|
248 |
long bytesread, bytesreadtotal = 0, byteswritetotal = 0;
|
|
|
249 |
SizeT buffoutreslen;
|
|
|
250 |
ISzAlloc g_alloc;
|
|
|
251 |
ELzmaStatus lzmastatus;
|
|
|
252 |
SRes lzmaresult;
|
|
|
253 |
CLzmaDec lzmahandle;
|
|
|
254 |
unsigned char lzmahdr[LZMA_PROPS_SIZE]; /* 5 bytes of properties */
|
|
|
255 |
unsigned char *lzmaoutbuff;
|
|
|
256 |
|
|
|
257 |
extract_res = -5; /* assume we will fail. if we don't - then we will update this flag */
|
|
|
258 |
lzmaoutbuff = malloc(lzmaoutbufflen);
|
|
|
259 |
if (lzmaoutbuff == NULL) {
|
|
|
260 |
free(buff);
|
|
|
261 |
fclose(filefd); /* close the dst file */
|
|
|
262 |
return(-33);
|
|
|
263 |
}
|
|
|
264 |
|
|
|
265 |
fread(lzmahdr, 4, 1, zipfd); /* load the 4 bytes long 'zip-lzma header */
|
|
|
266 |
bytesreadtotal = 4; /* remember we read 4 bytes already */
|
|
|
267 |
|
|
|
268 |
/* lzma properties should be exactly 5 bytes long. If it's not, it's either not valid lzma, or some version that wasn't existing yet when I wrote these words. Also, check that the lzma content is at least 9 bytes long and that our previous malloc() calls suceeded. */
|
|
|
269 |
if ((lzmahdr[2] == 5) && (lzmahdr[3] == 0) && (curzipnode->compressedfilelen >= 9)) {
|
|
|
270 |
|
|
|
271 |
extract_res = 0; /* since we got so far, let's assume we will succeed now */
|
|
|
272 |
|
|
|
273 |
g_alloc.Alloc = SzAlloc; /* these will be used as callbacks by lzma to manage memory */
|
|
|
274 |
g_alloc.Free = SzFree;
|
|
|
275 |
|
|
|
276 |
fread(lzmahdr, sizeof(lzmahdr), 1, zipfd); /* load the lzma header */
|
|
|
277 |
bytesreadtotal += sizeof(lzmahdr);
|
|
|
278 |
|
|
|
279 |
/* Note, that in a 'normal' lzma stream we would have now 8 bytes with the uncompressed length of the file. Here we don't. ZIP cut this information out, since it stores it already in its own header. */
|
|
|
280 |
|
|
|
281 |
memset(&lzmahandle, 0, sizeof(lzmahandle)); /* reset the whole lzmahandle structure - not doing this leads to CRASHES!!! */
|
|
|
282 |
LzmaDec_Init(&lzmahandle);
|
|
|
283 |
lzmaresult = LzmaDec_Allocate(&lzmahandle, lzmahdr, LZMA_PROPS_SIZE, &g_alloc); /* forget not to LzmaDec_Free() later! */
|
|
|
284 |
if (lzmaresult != 0) extract_res = -13;
|
|
|
285 |
|
|
|
286 |
while (extract_res == 0) {
|
|
|
287 |
bytesread = buffsize;
|
|
|
288 |
if (bytesread > curzipnode->compressedfilelen - bytesreadtotal) bytesread = curzipnode->compressedfilelen - bytesreadtotal;
|
|
|
289 |
buffoutreslen = lzmaoutbufflen;
|
|
|
290 |
/* printf("Will read %d bytes from input stream\n", bytesread); */
|
|
|
291 |
fread(buff, bytesread, 1, zipfd); /* read stuff from input stream */
|
|
|
292 |
fseek(zipfd, 0 - bytesread, SEEK_CUR); /* get back to the position at the start of our chunk of data */
|
|
|
293 |
lzmaresult = LzmaDec_DecodeToBuf(&lzmahandle, lzmaoutbuff, &buffoutreslen, buff, (SizeT *)&bytesread, LZMA_FINISH_ANY, &lzmastatus);
|
|
|
294 |
bytesreadtotal += bytesread;
|
|
|
295 |
/* printf("expanded %ld bytes into %ld (total read: %ld bytes)\n", (long)bytesread, (long)buffoutreslen, (long)bytesreadtotal); */
|
|
|
296 |
fseek(zipfd, bytesread, SEEK_CUR); /* go forward to the position next to the input we processed */
|
|
|
297 |
if (lzmaresult != SZ_OK) {
|
|
|
298 |
extract_res = -20;
|
|
|
299 |
if (lzmaresult == SZ_ERROR_DATA) extract_res = -21; /* DATA ERROR */
|
|
|
300 |
if (lzmaresult == SZ_ERROR_MEM) extract_res = -22; /* MEMORY ALLOC ERROR */
|
|
|
301 |
if (lzmaresult == SZ_ERROR_UNSUPPORTED) extract_res = -23; /* UNSUPPORTED PROPERTY */
|
|
|
302 |
if (lzmaresult == SZ_ERROR_INPUT_EOF) extract_res = -24; /* NEED MORE INPUT */
|
|
|
303 |
break;
|
|
|
304 |
}
|
|
|
305 |
/* check that we haven't got TOO MUCH decompressed data, and trim if necessary. It happens that LZMA provides a few bytes more than it should at the end of the stream. */
|
|
|
306 |
if (byteswritetotal + (long)buffoutreslen > curzipnode->filelen) {
|
|
|
307 |
buffoutreslen = curzipnode->filelen - byteswritetotal;
|
|
|
308 |
}
|
|
|
309 |
byteswritetotal += buffoutreslen;
|
|
|
310 |
fwrite(lzmaoutbuff, buffoutreslen, 1, filefd); /* write stuff to output file */
|
|
|
311 |
crc32_feed(&cksum, lzmaoutbuff, buffoutreslen);
|
|
|
312 |
/* if (lzmastatus == LZMA_STATUS_FINISHED_WITH_MARK) puts("lzma says we are done!"); */
|
|
|
313 |
if ((lzmastatus == LZMA_STATUS_FINISHED_WITH_MARK) || (bytesreadtotal >= curzipnode->compressedfilelen)) {
|
|
|
314 |
extract_res = 0; /* looks like we succeeded! */
|
|
|
315 |
break;
|
|
|
316 |
}
|
|
|
317 |
}
|
|
|
318 |
LzmaDec_Free(&lzmahandle, &g_alloc); /* this will free all the stuff we allocated via LzmaDec_Allocate() */
|
|
|
319 |
/* printf("Processed %d bytes of input into %d bytes of output. CRC32: %08lX\n", bytesreadtotal, byteswritetotal, crc32); */
|
|
|
320 |
}
|
|
|
321 |
free(lzmaoutbuff);
|
|
|
322 |
#endif
|
|
|
323 |
}
|
|
|
324 |
|
|
|
325 |
/* clean up memory, close the dst file and terminates crc32 */
|
|
|
326 |
free(buff);
|
|
|
327 |
fclose(filefd); /* close the dst file */
|
|
|
328 |
crc32_finish(&cksum);
|
|
|
329 |
|
|
|
330 |
/* printf("extract_res=%d / cksum_expected=%08lX / cksum_obtained=%08lX\n", extract_res, curzipnode->crc32, cksum); */
|
|
|
331 |
if (extract_res != 0) { /* was the extraction process successful? */
|
|
|
332 |
unlink(fulldestfilename); /* remove the failed file */
|
|
|
333 |
return(extract_res);
|
|
|
334 |
}
|
|
|
335 |
if (cksum != curzipnode->crc32) { /* is the crc32 ok after extraction? */
|
|
|
336 |
unlink(fulldestfilename); /* remove the failed file */
|
|
|
337 |
return(-9);
|
|
|
338 |
}
|
|
|
339 |
/* Set the timestamp of the new file to what was set in the zip file */
|
|
|
340 |
filetimestamp.actime = curzipnode->timestamp;
|
|
|
341 |
filetimestamp.modtime = curzipnode->timestamp;
|
|
|
342 |
utime(fulldestfilename, &filetimestamp);
|
|
|
343 |
return(0);
|
|
|
344 |
}
|
|
|
345 |
|
|
|
346 |
|
|
|
347 |
|
|
|
348 |
/* Call this to free a ziplist computed by zip_listfiles() */
|
|
|
349 |
void zip_freelist(struct ziplist **ziplist) {
|
|
|
350 |
struct ziplist *zipentrytobefreed;
|
|
|
351 |
while (*ziplist != NULL) { /* iterate through the linked list and free all nodes */
|
|
|
352 |
zipentrytobefreed = *ziplist;
|
|
|
353 |
*ziplist = zipentrytobefreed->nextfile;
|
|
|
354 |
/* free the node entry */
|
|
|
355 |
free(zipentrytobefreed);
|
|
|
356 |
}
|
|
|
357 |
*ziplist = NULL;
|
|
|
358 |
}
|