Rev 7869 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
7869 | leency | 1 | /* # KolibriOS Image Unpacker # |
2 | |||
3 | Extracts files from FAT12 KolibriOS image to specified folder. |
||
4 | |||
5 | Usage: unimg path/to/img [output/folder] [-e] |
||
6 | -e: Exit on success |
||
7 | If output folder is skipped, the image will be unpacked at /TMP0/1/[file-name] |
||
8 | |||
9 | Author: Magomed Kostoev (Boppan, mkostoevr): FAT12 file system, driver. |
||
10 | Contributor: Kiril Lipatov (Leency) */ |
||
11 | |||
7882 | Boppan | 12 | #ifdef __TINYC__ |
13 | # define TCC 1 |
||
14 | #else |
||
15 | # define GCC 1 |
||
16 | #endif |
||
17 | |||
7843 | Boppan | 18 | #include |
19 | #include |
||
20 | #include |
||
21 | #include |
||
22 | #include |
||
23 | |||
7882 | Boppan | 24 | #if TCC |
25 | # include |
||
26 | # define printf con_printf |
||
27 | # define puts con_write_asciiz |
||
28 | #else |
||
29 | # define con_init_console_dll() 0 |
||
30 | # define con_set_title(s) |
||
31 | # define con_exit(close) |
||
7869 | leency | 32 | #endif |
33 | |||
7843 | Boppan | 34 | typedef struct { |
35 | size_t length; |
||
36 | size_t capacity; |
||
37 | char *data; |
||
38 | } String; |
||
39 | |||
40 | typedef struct { |
||
41 | char *image; |
||
42 | int imageSize; |
||
43 | const char *errorMessage; |
||
44 | int bytesPerSector; |
||
45 | int sectorsPerClaster; |
||
46 | int reservedSectorCount; |
||
47 | int numberOfFats; |
||
48 | int maxRootEntries; |
||
49 | int totalSectors; |
||
50 | int sectorsPerFat; |
||
51 | int firstFat; |
||
52 | int rootDirectory; |
||
53 | int dataRegion; |
||
54 | } Fat12; |
||
55 | |||
56 | typedef int (*ForEachCallback)(const char *, size_t, const uint8_t *, void *); |
||
57 | |||
58 | // system-dependent |
||
59 | static void mkdir(const char *name); |
||
60 | // misc |
||
61 | static void mkdir_p(const char *_name); // create folder creating its parents |
||
62 | static uint16_t get16(const void *_from, int index); // get uint16_t from array at offset |
||
63 | static uint32_t get32(const void *_from, int index); // get uint32_t from array at offset |
||
64 | // fat12 |
||
65 | static int fat12__getItemNameSize(const void *_folderEntry); |
||
66 | static void fat12__getItemName(const void *_folderEntry, void *_name); |
||
67 | static int fat12__getNextClaster(const Fat12 *this, int currentClaster); |
||
68 | static int fat12__getFile(const Fat12 *this, void *_buffer, int size, int claster); |
||
69 | static int fat12__getOffsetByClaster(const Fat12 *this, int claster); |
||
70 | static int fat12__forEachFile_handleFolderEntry(const Fat12 *this, int folderEntryOffset, String *name, |
||
71 | ForEachCallback callback, void *callbackParam); |
||
72 | static int fat12__forEachFile_handleFolder(const Fat12 *this, int claster, String *name, |
||
73 | ForEachCallback callback, void *callbackParam); |
||
74 | static int fat12__forEachFile(const Fat12 *this, ForEachCallback callback, void *callbackParam); |
||
75 | static int fat12__open(Fat12 *this, const char *img); |
||
76 | static int fat12__error(Fat12 *this, const char *errorMessage); |
||
77 | |||
78 | static void mkdir(const char *name) { |
||
79 | struct { |
||
80 | int fn; |
||
81 | int unused[4]; |
||
82 | char b; |
||
7869 | leency | 83 | const char *path __attribute__((packed)); |
84 | } info; |
||
7882 | Boppan | 85 | |
7843 | Boppan | 86 | memset(&info, 0, sizeof(info)); |
87 | info.fn = 9; |
||
88 | info.b = 0; |
||
89 | info.path = name; |
||
90 | asm volatile ("int $0x40"::"a"(70), "b"(&info)); |
||
91 | } |
||
92 | |||
93 | static void mkdir_p(const char *_name) { |
||
94 | char *name = calloc(strlen(_name) + 1, 1); |
||
95 | |||
96 | strcpy(name, _name); |
||
97 | char *ptr = name; |
||
98 | while (ptr) { |
||
99 | if (ptr != name) { *ptr = '/'; } |
||
100 | ptr = strchr(ptr + 1, '/'); |
||
101 | if (ptr) { *ptr = 0; } |
||
102 | mkdir(name); |
||
103 | } |
||
104 | } |
||
105 | |||
106 | static uint32_t get32(const void *_from, int index) { |
||
107 | const uint8_t *from = _from; |
||
108 | return from[index] | |
||
109 | (from[index + 1] << 8) | |
||
110 | (from[index + 2] << 16) | |
||
111 | (from[index + 3] << 24); |
||
112 | } |
||
113 | |||
114 | static uint16_t get16(const void *_from, int index) { |
||
115 | const uint8_t *from = _from; |
||
116 | |||
117 | return from[index] | (from[index + 1] << 8); |
||
118 | } |
||
119 | |||
120 | static int fat12__getNextClaster(const Fat12 *this, int currentClaster) { |
||
121 | int nextClasterOffset = this->firstFat + currentClaster + (currentClaster >> 1); |
||
122 | |||
123 | if (currentClaster % 2 == 0) { |
||
124 | return get16(this->image, nextClasterOffset) & 0xfff; |
||
125 | } else { |
||
126 | return get16(this->image, nextClasterOffset) >> 4; |
||
127 | } |
||
128 | } |
||
129 | |||
130 | static int fat12__getFile(const Fat12 *this, void *_buffer, int size, int claster) { |
||
131 | int offset = 0; |
||
132 | char *buffer = _buffer; |
||
133 | |||
134 | while (claster < 0xff7) { |
||
135 | int toCopy = this->bytesPerSector * this->sectorsPerClaster; |
||
136 | void *clasterPtr = &this->image[fat12__getOffsetByClaster(this, claster)]; |
||
137 | |||
138 | claster = fat12__getNextClaster(this, claster); |
||
139 | // if next claster is END OF FILE claster, copy only rest of file |
||
140 | if (claster >= 0xff7) { toCopy = size % toCopy; } |
||
141 | memcpy(&buffer[offset], clasterPtr, toCopy); |
||
142 | offset += toCopy; |
||
143 | } |
||
144 | return 1; |
||
145 | } |
||
146 | |||
147 | static int fat12__getOffsetByClaster(const Fat12 *this, int claster) { |
||
148 | return this->dataRegion + (claster - 2) |
||
149 | * this->bytesPerSector * this->sectorsPerClaster; |
||
150 | } |
||
151 | |||
152 | static int fat12__getItemNameSize(const void *_folderEntry) { |
||
153 | const uint8_t *folderEntry = _folderEntry; |
||
154 | |||
155 | // Long File Name entry, not a file itself |
||
156 | if ((folderEntry[11] & 0x0f) == 0x0f) { return 0; } |
||
157 | if ((folderEntry[11 - 32] & 0x0f) != 0x0f) { |
||
158 | // regular file "NAME8888" '.' "EXT" '\0' |
||
159 | int length = 13; |
||
160 | |||
161 | for (int i = 10; folderEntry[i] == ' ' && i != 7; i--) { length--; } |
||
162 | for (int i = 7; folderEntry[i] == ' ' && i != 0 - 1; i--) { length--; } |
||
163 | if (folderEntry[8] == ' ') { length--; } // no ext - no'.' |
||
164 | return length; |
||
165 | } else { |
||
166 | // file with long name |
||
167 | // format of Long File Name etries is described in fat12__getItemName |
||
168 | int length = 1; |
||
169 | |||
170 | for (int i = 1; i < 255 / 13; i++) { |
||
171 | //! TODO: Add UTF-16 support |
||
172 | length += 13; |
||
173 | if (folderEntry[i * -32] & 0x40) { |
||
174 | // if first char from back is 0xffff, this is stub after name |
||
175 | // otherwice is last character, so we can return calculated length |
||
176 | if (get16(folderEntry, i * -32 + 30) == 0xffff) { length--; } else { return length; } |
||
177 | if (get16(folderEntry, i * -32 + 28) == 0xffff) { length--; } else { return length; } |
||
178 | if (get16(folderEntry, i * -32 + 24) == 0xffff) { length--; } else { return length; } |
||
179 | if (get16(folderEntry, i * -32 + 22) == 0xffff) { length--; } else { return length; } |
||
180 | if (get16(folderEntry, i * -32 + 20) == 0xffff) { length--; } else { return length; } |
||
181 | if (get16(folderEntry, i * -32 + 18) == 0xffff) { length--; } else { return length; } |
||
182 | if (get16(folderEntry, i * -32 + 16) == 0xffff) { length--; } else { return length; } |
||
183 | if (get16(folderEntry, i * -32 + 14) == 0xffff) { length--; } else { return length; } |
||
184 | if (get16(folderEntry, i * -32 + 9) == 0xffff) { length--; } else { return length; } |
||
185 | if (get16(folderEntry, i * -32 + 7) == 0xffff) { length--; } else { return length; } |
||
186 | if (get16(folderEntry, i * -32 + 5) == 0xffff) { length--; } else { return length; } |
||
187 | if (get16(folderEntry, i * -32 + 3) == 0xffff) { length--; } else { return length; } |
||
188 | if (get16(folderEntry, i * -32 + 1) == 0xffff) { length--; } else { return length; } |
||
189 | return length; |
||
190 | } |
||
191 | } |
||
192 | } |
||
193 | return 0; // WAT? |
||
194 | } |
||
195 | |||
196 | static void fat12__getItemName(const void *_folderEntry, void *_name) { |
||
197 | const uint8_t *folderEntry = _folderEntry; |
||
198 | uint8_t *name = _name; |
||
199 | |||
200 | if ((folderEntry[11 - 32] & 0x0f) != 0x0f) { |
||
201 | int length = 8; |
||
202 | |||
203 | memset(name, 0, 13); |
||
204 | memcpy(name, folderEntry, 8); |
||
205 | while (name[length - 1] == ' ') { length--; } |
||
206 | if (folderEntry[9] != ' ') { |
||
207 | name[length++] = '.'; |
||
208 | memcpy(&name[length], &folderEntry[8], 3); |
||
209 | length += 3; |
||
210 | } |
||
211 | while (name[length - 1] == ' ') { length--; } |
||
212 | name[length] = '\0'; |
||
213 | } else { |
||
214 | // previous folder entries hold long name in format: |
||
215 | // 0 sequence nmber (in turn back to first Long File Name entry, from 1) |
||
216 | // 1 - 10 file name next characters in utf-16 |
||
217 | // 11 file attributes (0x0f - LFN entry) |
||
218 | // 12 reserved |
||
219 | // 13 checksum |
||
220 | // 14 - 25 file name next characters |
||
221 | // 26 - 27 reserved |
||
222 | // 28 - 31 file name next characters |
||
223 | // in these entries name placed in sequential order |
||
224 | // but first characters are located in first previous entry |
||
225 | // next characters - in next previous etc. |
||
226 | // if current entry is orificated by 0x40 - the entry is last (cinains last characters) |
||
227 | // unneed places for characters in the last entry are filled by 0xff |
||
228 | int length = 0; |
||
229 | |||
230 | for (int i = 1; i < 255 / 13; i++) { |
||
231 | //! TODO: Add unicode support |
||
232 | name[length++] = folderEntry[i * -32 + 1]; |
||
233 | name[length++] = folderEntry[i * -32 + 3]; |
||
234 | name[length++] = folderEntry[i * -32 + 5]; |
||
235 | name[length++] = folderEntry[i * -32 + 7]; |
||
236 | name[length++] = folderEntry[i * -32 + 9]; |
||
237 | name[length++] = folderEntry[i * -32 + 14]; |
||
238 | name[length++] = folderEntry[i * -32 + 16]; |
||
239 | name[length++] = folderEntry[i * -32 + 18]; |
||
240 | name[length++] = folderEntry[i * -32 + 20]; |
||
241 | name[length++] = folderEntry[i * -32 + 22]; |
||
242 | name[length++] = folderEntry[i * -32 + 24]; |
||
243 | name[length++] = folderEntry[i * -32 + 28]; |
||
244 | name[length++] = folderEntry[i * -32 + 30]; |
||
245 | if (folderEntry[i * -32] & 0x40) { |
||
246 | while (name[length - 1] == 0xff) { name[--length] = 0; } |
||
247 | name[length++] = 0; |
||
248 | return; |
||
249 | } |
||
250 | } |
||
251 | } |
||
252 | } |
||
253 | |||
254 | |||
255 | static int fat12__forEachFile_handleFolderEntry(const Fat12 *this, int folderEntryOffset, String *name, |
||
256 | ForEachCallback callback, void *callbackParam) { |
||
257 | int nameSize = 0; |
||
258 | |||
259 | if (this->image[folderEntryOffset] == 0) { return 1; } // zero-entry, not file nor folder |
||
260 | nameSize = fat12__getItemNameSize(&this->image[folderEntryOffset]); // includes sizeof '\0' |
||
261 | if (nameSize != 0) { |
||
262 | while (name->capacity < name->length + nameSize + 1) { |
||
263 | name->capacity += name->capacity / 2; |
||
264 | name->data = realloc(name->data, name->capacity); |
||
265 | } |
||
266 | name->data[name->length++] = '/'; |
||
267 | fat12__getItemName(&this->image[folderEntryOffset], &name->data[name->length]); |
||
268 | name->length += nameSize - 1; |
||
269 | if ((this->image[folderEntryOffset + 11] & 0x10)) { |
||
270 | // the item is folder |
||
271 | // handle folder only if it isn't current folder or parent one |
||
272 | if (memcmp(&this->image[folderEntryOffset], ". ", 11) && |
||
273 | memcmp(&this->image[folderEntryOffset], ".. ", 11)) { |
||
274 | if (!fat12__forEachFile_handleFolder(this, get16(this->image, folderEntryOffset + 26), name, callback, callbackParam)) { |
||
275 | return 0; |
||
276 | } |
||
277 | } |
||
278 | } else { |
||
279 | // the item is a regular file |
||
280 | void *buffer = NULL; |
||
281 | int size = get32(this->image, folderEntryOffset + 28); |
||
282 | int cluster = get16(this->image, folderEntryOffset + 26); |
||
283 | |||
284 | buffer = malloc(size); |
||
285 | if (!fat12__getFile(this, buffer, size, cluster)) { |
||
286 | free(buffer); |
||
287 | return 0; |
||
288 | } |
||
289 | callback(name->data, size, buffer, callbackParam); |
||
290 | free(buffer); |
||
291 | } |
||
292 | name->length -= nameSize - 1; // substract length of current item name |
||
293 | name->length--; // substract length of '/' |
||
294 | name->data[name->length] = '\0'; |
||
295 | } |
||
296 | return 1; |
||
297 | } |
||
298 | |||
299 | static int fat12__forEachFile_handleFolder(const Fat12 *this, int claster, String *name, |
||
300 | ForEachCallback callback, void *callbackParam) { |
||
301 | for (; claster < 0xff7; claster = fat12__getNextClaster(this, claster)) { |
||
302 | int offset = fat12__getOffsetByClaster(this, claster); |
||
303 | |||
304 | for (int i = 0; i < (this->bytesPerSector * this->sectorsPerClaster / 32); i++) { |
||
305 | if (!fat12__forEachFile_handleFolderEntry(this, offset + 32 * i, name, callback, callbackParam)) { |
||
306 | return 0; |
||
307 | } |
||
308 | } |
||
309 | } |
||
310 | return 1; |
||
311 | } |
||
312 | |||
313 | static int fat12__forEachFile(const Fat12 *this, ForEachCallback callback, void *callbackParam) { |
||
314 | String name = { 0 }; |
||
315 | |||
316 | name.capacity = 4096; |
||
317 | name.data = malloc(name.capacity); |
||
318 | name.length = 0; |
||
319 | name.data[0] = '\0'; |
||
320 | |||
321 | for (int i = 0; i < this->maxRootEntries; i++) { |
||
322 | if (!fat12__forEachFile_handleFolderEntry(this, this->rootDirectory + 32 * i, &name, callback, callbackParam)) { |
||
323 | free(name.data); |
||
324 | return 0; |
||
325 | } |
||
326 | } |
||
327 | free(name.data); |
||
328 | return 1; |
||
329 | } |
||
330 | |||
331 | static int fat12__open(Fat12 *this, const char *img) { |
||
332 | FILE *fp = NULL; |
||
333 | |||
334 | if (!(fp = fopen(img, "rb"))) { |
||
335 | return fat12__error(this, "Can't open imput file"); |
||
336 | } |
||
337 | fseek(fp, 0, SEEK_END); |
||
338 | this->imageSize = ftell(fp); |
||
339 | rewind(fp); |
||
340 | if (!(this->image = malloc(this->imageSize))) { |
||
341 | return fat12__error(this, "Can't allocate memory for image"); |
||
342 | } |
||
343 | fread(this->image, 1, this->imageSize, fp); |
||
344 | fclose(fp); |
||
345 | this->bytesPerSector = *(uint16_t *)((uintptr_t)this->image + 11); |
||
346 | this->sectorsPerClaster = *(uint8_t *)((uintptr_t)this->image + 0x0d); |
||
347 | this->reservedSectorCount = *(uint16_t *)((uintptr_t)this->image + 0x0e); |
||
348 | this->numberOfFats = *(uint8_t *)((uintptr_t)this->image + 0x10); |
||
349 | this->maxRootEntries = *(uint16_t *)((uintptr_t)this->image + 0x11); |
||
350 | this->totalSectors = *(uint16_t *)((uintptr_t)this->image + 0x13); |
||
351 | if (!this->totalSectors) { |
||
352 | this->totalSectors = *(uint32_t *)((uintptr_t)this->image + 0x20); |
||
353 | } |
||
354 | this->sectorsPerFat = *(uint16_t *)((uintptr_t)this->image + 0x16); |
||
355 | this->firstFat = (0 + this->reservedSectorCount) * this->bytesPerSector; |
||
356 | this->rootDirectory = this->firstFat + this->numberOfFats |
||
357 | * this->sectorsPerFat * this->bytesPerSector; |
||
358 | this->dataRegion = this->rootDirectory + this->maxRootEntries * 32; |
||
7868 | Boppan | 359 | printf("\nBytes per sector: %d\n", this->bytesPerSector); |
360 | printf("Sectors per claster: %d\n", this->sectorsPerClaster); |
||
361 | printf("Reserver sector count: %d\n", this->reservedSectorCount); |
||
362 | printf("Number of FATs: %d\n", this->numberOfFats); |
||
363 | printf("Max root entries: %d\n", this->maxRootEntries); |
||
364 | printf("Total sectors: %d\n", this->totalSectors); |
||
365 | printf("Sectors per FAT: %d\n", this->sectorsPerFat); |
||
366 | printf("First FAT: %d\n", this->firstFat); |
||
367 | printf("Root directory: %d\n", this->rootDirectory); |
||
368 | printf("Data region: %d\n\n", this->dataRegion); |
||
7843 | Boppan | 369 | return 1; |
370 | } |
||
371 | |||
372 | static int fat12__error(Fat12 *this, const char *errorMessage) { |
||
373 | this->errorMessage = errorMessage; |
||
374 | return 0; |
||
375 | } |
||
376 | |||
377 | static int handleError(const Fat12 *fat12) { |
||
7868 | Boppan | 378 | printf("Error in Fat12: %s\n", fat12->errorMessage); |
7882 | Boppan | 379 | con_exit(0); |
7843 | Boppan | 380 | return -1; |
381 | } |
||
382 | |||
7868 | Boppan | 383 | void writeFile(const char *fileName, int size, const uint8_t *data) { |
7882 | Boppan | 384 | #if TCC |
7868 | Boppan | 385 | struct Info { |
386 | int number; |
||
387 | int reserved0; |
||
388 | int reserved1; |
||
389 | int dataSize; |
||
390 | const void *data; |
||
391 | char zero; |
||
7882 | Boppan | 392 | const char *name __attribute__((packed)); |
393 | } info; |
||
394 | |||
395 | memset(&info, 0, sizeof(struct Info)); |
||
396 | info.number = 2; // create/overwrite file |
||
397 | info.dataSize = size; |
||
398 | info.data = data; |
||
399 | info.zero = 0; |
||
400 | info.name = fileName; |
||
401 | asm volatile ("int $0x40" :: "a"(70), "b"(&info)); |
||
402 | #else |
||
403 | FILE *fp = NULL; |
||
404 | if (!(fp = fopen(fileName, "wb"))) { perror(NULL); } |
||
405 | fwrite(data, 1, size, fp); |
||
406 | fclose(fp); |
||
407 | #endif |
||
7868 | Boppan | 408 | } |
409 | |||
7843 | Boppan | 410 | static int callback(const char *name, size_t size, const uint8_t *data, void *param) { |
411 | String *outputPath = param; |
||
412 | |||
413 | while (outputPath->capacity < outputPath->length + strlen(name) + 1 + 1) { |
||
414 | outputPath->capacity += outputPath->capacity / 2; |
||
415 | outputPath->data = realloc(outputPath->data, outputPath->capacity); |
||
416 | } |
||
417 | strcat(outputPath->data, name); |
||
418 | { // don't let mkdir_p create folder where file should be located |
||
419 | char *fileNameDelim = NULL; |
||
420 | |||
421 | // no slash = no folders to create, outputPath->data contains only file name |
||
422 | // yes, I know, outputPath->data always contains '/', but who knows... |
||
423 | if ((fileNameDelim = strrchr(outputPath->data, '/'))) { |
||
424 | *fileNameDelim = '\0'; |
||
425 | mkdir_p(outputPath->data); |
||
426 | *fileNameDelim = '/'; |
||
427 | } |
||
428 | } |
||
7868 | Boppan | 429 | printf("Extracting %s\n", outputPath->data); |
7882 | Boppan | 430 | writeFile(outputPath->data, size, data); |
7843 | Boppan | 431 | outputPath->data[outputPath->length] = '\0'; |
432 | return 0; |
||
433 | } |
||
434 | |||
435 | |||
436 | |||
7869 | leency | 437 | int main(int argc, char* argv[]) { |
7843 | Boppan | 438 | Fat12 fat12 = { 0 }; |
439 | char *imageFile = NULL; |
||
440 | String outputFolder = { 0 }; |
||
7882 | Boppan | 441 | int closeOnExit = 0; |
7843 | Boppan | 442 | |
7882 | Boppan | 443 | if (con_init_console_dll()) { return -1; } |
444 | con_set_title("UnImg - kolibri.img file unpacker"); |
||
7869 | leency | 445 | |
7843 | Boppan | 446 | if (argc < 2) { |
7869 | leency | 447 | puts(" Usage:"); |
448 | puts(" unimg \"/path/to/kolibri.img\" \"/optional/extract/path\""); |
||
449 | puts(" where optional key [-e] is exit on success"); |
||
7882 | Boppan | 450 | con_exit(0); |
7843 | Boppan | 451 | return -1; |
7867 | leency | 452 | } else { |
7882 | Boppan | 453 | imageFile = argv[1]; |
454 | printf("File: %s\n", imageFile); |
||
7843 | Boppan | 455 | } |
456 | |||
457 | outputFolder.capacity = 4096; |
||
458 | outputFolder.data = malloc(outputFolder.capacity); |
||
459 | |||
460 | //! ACHTUNG: possible buffer overflow, is 4096 enough in KolibriOS? |
||
7882 | Boppan | 461 | if (argc >= 3 && argv[2][0] != '-') { strcpy(outputFolder.data, argv[2]); } |
7867 | leency | 462 | else { |
7882 | Boppan | 463 | strcpy(outputFolder.data, "/tmp0/1"); |
464 | strcat(outputFolder.data, strrchr(imageFile, '/')); |
||
7867 | leency | 465 | } |
7843 | Boppan | 466 | |
467 | outputFolder.length = strlen(outputFolder.data); |
||
468 | |||
7869 | leency | 469 | // handle -e parameter - exit on success |
7882 | Boppan | 470 | if (argc >= 3 && !strcmp(argv[argc - 1], "-e")) { closeOnExit = 1; } |
7869 | leency | 471 | |
7882 | Boppan | 472 | if (!fat12__open(&fat12, imageFile)) { |
473 | return handleError(&fat12); |
||
7843 | Boppan | 474 | } |
475 | if (!fat12__forEachFile(&fat12, callback, &outputFolder)) { |
||
476 | return handleError(&fat12); |
||
477 | } |
||
478 | |||
7869 | leency | 479 | puts("\nDONE!"); |
7882 | Boppan | 480 | con_exit(closeOnExit); |
481 | return 0; |
||
7843 | Boppan | 482 | }>>>>>>>>>><>><>><>><> |