Rev 7868 | Go to most recent revision | 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 | |||
7843 | Boppan | 12 | #include |
13 | #include |
||
14 | #include |
||
15 | #include |
||
16 | #include |
||
17 | |||
7869 | leency | 18 | #define TCC 0 |
19 | #define GCC 1 |
||
20 | #include "compiller.h" |
||
21 | |||
22 | #ifdef GCC |
||
23 | #define con_init con_init |
||
24 | #endif |
||
25 | |||
7843 | Boppan | 26 | typedef struct { |
27 | size_t length; |
||
28 | size_t capacity; |
||
29 | char *data; |
||
30 | } String; |
||
31 | |||
32 | typedef struct { |
||
33 | char *image; |
||
34 | int imageSize; |
||
35 | const char *errorMessage; |
||
36 | int bytesPerSector; |
||
37 | int sectorsPerClaster; |
||
38 | int reservedSectorCount; |
||
39 | int numberOfFats; |
||
40 | int maxRootEntries; |
||
41 | int totalSectors; |
||
42 | int sectorsPerFat; |
||
43 | int firstFat; |
||
44 | int rootDirectory; |
||
45 | int dataRegion; |
||
46 | } Fat12; |
||
47 | |||
48 | typedef int (*ForEachCallback)(const char *, size_t, const uint8_t *, void *); |
||
49 | |||
50 | // system-dependent |
||
51 | static void mkdir(const char *name); |
||
52 | // misc |
||
53 | static void mkdir_p(const char *_name); // create folder creating its parents |
||
54 | static uint16_t get16(const void *_from, int index); // get uint16_t from array at offset |
||
55 | static uint32_t get32(const void *_from, int index); // get uint32_t from array at offset |
||
56 | // fat12 |
||
57 | static int fat12__getItemNameSize(const void *_folderEntry); |
||
58 | static void fat12__getItemName(const void *_folderEntry, void *_name); |
||
59 | static int fat12__getNextClaster(const Fat12 *this, int currentClaster); |
||
60 | static int fat12__getFile(const Fat12 *this, void *_buffer, int size, int claster); |
||
61 | static int fat12__getOffsetByClaster(const Fat12 *this, int claster); |
||
62 | static int fat12__forEachFile_handleFolderEntry(const Fat12 *this, int folderEntryOffset, String *name, |
||
63 | ForEachCallback callback, void *callbackParam); |
||
64 | static int fat12__forEachFile_handleFolder(const Fat12 *this, int claster, String *name, |
||
65 | ForEachCallback callback, void *callbackParam); |
||
66 | static int fat12__forEachFile(const Fat12 *this, ForEachCallback callback, void *callbackParam); |
||
67 | static int fat12__open(Fat12 *this, const char *img); |
||
68 | static int fat12__error(Fat12 *this, const char *errorMessage); |
||
69 | |||
70 | static void mkdir(const char *name) { |
||
7869 | leency | 71 | #ifdef TCC |
7843 | Boppan | 72 | struct { |
73 | int fn; |
||
74 | int unused[4]; |
||
75 | char b; |
||
7869 | leency | 76 | const char *path __attribute__((packed)); |
77 | } info; |
||
78 | #else |
||
79 | struct { |
||
80 | int fn; |
||
81 | int unused[4]; |
||
82 | char b; |
||
7868 | Boppan | 83 | const char *path; |
7869 | leency | 84 | } __attribute__((packed)) info; |
85 | #endif |
||
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); |
7843 | Boppan | 379 | return -1; |
380 | } |
||
381 | |||
7868 | Boppan | 382 | void writeFile(const char *fileName, int size, const uint8_t *data) { |
383 | // FILE *fp = NULL; |
||
384 | // if (!(fp = fopen(fileName, "wb"))) { perror(NULL); } |
||
385 | // fwrite(data, 1, size, fp); |
||
386 | // fclose(fp); |
||
387 | struct Info { |
||
388 | int number; |
||
389 | int reserved0; |
||
390 | int reserved1; |
||
391 | int dataSize; |
||
392 | const void *data; |
||
393 | char zero; |
||
394 | const char *name; |
||
395 | } __attribute__((packed)) *info = calloc(sizeof(struct Info), 1); |
||
396 | |||
397 | info->number = 2; // create/overwrite file |
||
398 | info->dataSize = size; |
||
399 | info->data = data; |
||
400 | info->zero = 0; |
||
401 | info->name = fileName; |
||
402 | asm volatile ("int $0x40" :: "a"(70), "b"(info)); |
||
403 | } |
||
404 | |||
7843 | Boppan | 405 | static int callback(const char *name, size_t size, const uint8_t *data, void *param) { |
406 | String *outputPath = param; |
||
407 | |||
408 | while (outputPath->capacity < outputPath->length + strlen(name) + 1 + 1) { |
||
409 | outputPath->capacity += outputPath->capacity / 2; |
||
410 | outputPath->data = realloc(outputPath->data, outputPath->capacity); |
||
411 | } |
||
412 | strcat(outputPath->data, name); |
||
413 | { // don't let mkdir_p create folder where file should be located |
||
414 | char *fileNameDelim = NULL; |
||
415 | |||
416 | // no slash = no folders to create, outputPath->data contains only file name |
||
417 | // yes, I know, outputPath->data always contains '/', but who knows... |
||
418 | if ((fileNameDelim = strrchr(outputPath->data, '/'))) { |
||
419 | *fileNameDelim = '\0'; |
||
420 | mkdir_p(outputPath->data); |
||
421 | *fileNameDelim = '/'; |
||
422 | } |
||
423 | } |
||
7868 | Boppan | 424 | printf("Extracting %s\n", outputPath->data); |
7869 | leency | 425 | #ifdef TCC |
426 | FILE *fp = NULL; |
||
427 | if (!(fp = fopen(outputPath->data, "wb"))) { perror(NULL); } |
||
428 | fwrite(data, 1, size, fp); |
||
429 | fclose(fp); |
||
430 | #else |
||
431 | writeFile(outputPath->data, size, data); |
||
432 | #endif |
||
7843 | Boppan | 433 | outputPath->data[outputPath->length] = '\0'; |
434 | return 0; |
||
435 | } |
||
436 | |||
437 | |||
438 | |||
7869 | leency | 439 | int main(int argc, char* argv[]) { |
7843 | Boppan | 440 | Fat12 fat12 = { 0 }; |
441 | char *imageFile = NULL; |
||
442 | String outputFolder = { 0 }; |
||
7869 | leency | 443 | int exit_code = 0; |
7843 | Boppan | 444 | |
7869 | leency | 445 | char app_title[] = "UnImg - kolibri.img file unpacker"; |
446 | con_init(-1, -1, -1, 350, app_title); |
||
447 | |||
7843 | Boppan | 448 | if (argc < 2) { |
7869 | leency | 449 | puts(" Usage:"); |
450 | puts(" unimg \"/path/to/kolibri.img\" \"/optional/extract/path\""); |
||
451 | puts(" where optional key [-e] is exit on success"); |
||
452 | exit(exit_code); |
||
7843 | Boppan | 453 | return -1; |
7867 | leency | 454 | } else { |
455 | imageFile = argv[1]; |
||
7868 | Boppan | 456 | printf("File: %s\n", imageFile); |
7843 | Boppan | 457 | } |
458 | |||
459 | outputFolder.capacity = 4096; |
||
460 | outputFolder.data = malloc(outputFolder.capacity); |
||
461 | |||
462 | //! ACHTUNG: possible buffer overflow, is 4096 enough in KolibriOS? |
||
463 | if (argc >= 3 && argv[2][0] != '-') strcpy(outputFolder.data, argv[2]); |
||
7867 | leency | 464 | else { |
465 | strcpy(outputFolder.data, "/tmp0/1"); |
||
466 | strcat(outputFolder.data, strrchr(imageFile, '/')); |
||
467 | } |
||
7843 | Boppan | 468 | |
469 | outputFolder.length = strlen(outputFolder.data); |
||
470 | |||
7869 | leency | 471 | // handle -e parameter - exit on success |
472 | if (argc >= 3 && !strcmp(argv[argc - 1], "-e")) { exit_code = 1; } |
||
473 | |||
7843 | Boppan | 474 | if (!fat12__open(&fat12, imageFile)) { |
475 | return handleError(&fat12); |
||
476 | } |
||
477 | |||
478 | if (!fat12__forEachFile(&fat12, callback, &outputFolder)) { |
||
479 | return handleError(&fat12); |
||
480 | } |
||
481 | |||
7869 | leency | 482 | puts("\nDONE!"); |
483 | exit(exit_code); |
||
7843 | Boppan | 484 | }>>>>>>>>>><>><>><>><> |