Add MGD to PNG extraction filter.
[fjfix.git] / fjfix.c
blobd83153089f86dc5c3e35eada080e2dcaa8a3fbd1
1 /* fjfix is a tool for handling FileJoin (FJSYS) archives.
3 In addition to pack/unpack functionality fixes file index on existing
4 archives to work with either WINE, Windows or POSIX string collation
5 order, allowing poorly coded applications run in WINE.
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <errno.h>
17 #include <getopt.h>
18 #include <limits.h>
19 #include <sys/stat.h>
21 #ifndef VERSION
22 #define VERSION "<devel>"
23 #endif
25 #ifdef _WIN32
26 #include <windows.h>
27 #define strcasecmp lstrcmpiA
29 #ifndef __WINE__
30 #include <dirent.h>
31 #define mkdir(n, p) mkdir(n)
32 #endif
34 #endif
36 #define FJHDR_SZ 0x54
38 struct fjhdr
40 char magic[8];
41 unsigned data_off;
42 unsigned names_size;
43 unsigned file_num;
44 unsigned char padding[FJHDR_SZ - 8 - sizeof(unsigned)*3];
47 struct fjtabent
49 unsigned name_off;
50 unsigned data_size;
51 unsigned data_off;
52 unsigned padding;
55 struct fjfile
57 char fjname[PATH_MAX];
58 FILE* file;
60 unsigned options;
62 struct fjhdr* hdr;
63 struct fjtabent* tab;
64 unsigned nent, tabsz;
66 char* names;
69 struct fjentry
71 struct fjfile* fjfile;
72 unsigned tabind;
73 unsigned seekpos;
76 enum {FJFILE_VERBOSE=0x1};
78 typedef struct fjfile* fjfile_t;
79 typedef struct fjentry* fjentry_t;
81 static void badmsg(const char* name, const char* msg)
83 fprintf(stderr, "Bad FJ archive '%s': %s\n", name, msg);
86 fjfile_t fjfile_open(const char* name)
88 fjfile_t s = malloc(sizeof(*s));
90 if (strlen(name) > sizeof(s->fjname))
91 return 0;
92 strcpy(s->fjname, name);
94 s->hdr = 0;
95 s->tab = 0;
96 s->nent = s->tabsz = 0;
97 s->names = 0;
98 s->options = 0;
100 s->file = fopen(name, "r+b");
101 if (!s->file) {
102 fprintf(stderr, "Can't open file '%s': %s\n", name, strerror(errno));
103 goto err;
106 s->hdr = malloc(sizeof(*s->hdr));
107 if (fread(s->hdr, sizeof(*s->hdr), 1, s->file) < 1) {
108 badmsg(name, "header too small");
109 goto err;
112 if (strncmp(s->hdr->magic, "FJSYS", 6)) {
113 badmsg(name, "bad magic");
114 goto err;
117 /* Limits arbitrary */
118 if (s->hdr->file_num > 65535 || s->hdr->names_size > 16*65536 - 1 || s->hdr->data_off > (1 << 20)) {
119 badmsg(name, "header sanity check failed");
120 goto err;
123 s->nent = s->hdr->file_num;
124 s->tabsz = s->nent;
125 s->tab = malloc(s->nent*sizeof(*s->tab));
126 if (fread(s->tab, sizeof(*s->tab), s->nent, s->file) < s->nent) {
127 badmsg(name, "incomplete entry table");
128 goto err;
131 int i;
132 for (i = 0; i < s->nent; i++) {
133 if (s->tab[i].name_off >= s->hdr->names_size) {
134 badmsg(name, "entry name offset outside name section");
135 goto err;
139 s->names = malloc(s->hdr->names_size);
140 if (fread(s->names, s->hdr->names_size, 1, s->file) < 1) {
141 badmsg(name, "incomplete name section");
142 goto err;
145 return s;
147 err:
148 if (s->file)
149 fclose(s->file);
150 if (s->hdr)
151 free(s->hdr);
152 if (s->tab)
153 free(s->tab);
154 if (s->names)
155 free(s->names);
156 free(s);
157 return 0;
160 const char* fjfile_name(fjfile_t s)
162 return s->fjname;
165 unsigned fjfile_set_options(fjfile_t s, unsigned flags)
167 return s->options |= flags;
170 unsigned fjfile_reset_options(fjfile_t s, unsigned flags)
172 return s->options &= ~flags;
175 fjentry_t fjfile_first_entry(fjfile_t fjfile)
177 fjentry_t s = malloc(sizeof(*s));
179 s->fjfile = fjfile;
180 s->tabind = 0;
181 s->seekpos = 0;
182 return s;
185 fjentry_t fjentry_next(fjentry_t s)
187 s->seekpos = 0;
189 if (++s->tabind < s->fjfile->nent)
190 return s;
191 free(s);
192 return 0;
195 const char* fjentry_name(fjentry_t s)
197 return s->fjfile->names + s->fjfile->tab[s->tabind].name_off;
200 unsigned fjentry_size(fjentry_t s)
202 return s->fjfile->tab[s->tabind].data_size;
205 int fjentry_seek(fjentry_t s, unsigned seekpos)
207 if (seekpos > s->fjfile->tab[s->tabind].data_size)
208 return -1;
210 s->seekpos = seekpos;
211 return 0;
214 unsigned fjentry_read(fjentry_t s, void* buf, unsigned size)
216 struct fjtabent* te = &s->fjfile->tab[s->tabind];
218 if (size > te->data_size - s->seekpos)
219 size = te->data_size - s->seekpos;
221 if (!size)
222 return 0;
224 if (fseek(s->fjfile->file, te->data_off + s->seekpos, SEEK_SET) < 0)
225 goto err;
227 if (fread(buf, size, 1, s->fjfile->file) != 1) {
228 if (feof(s->fjfile->file)) {
229 fprintf(stderr, "Reading entry '%s': unexpected EOF at 0x%08X + %d\n",
230 s->fjfile->names + te->name_off, s->seekpos, size);
231 return 0;
232 } else
233 goto err;
236 s->seekpos += size;
238 return size;
239 err:
240 fprintf(stderr, "Reading entry '%s' failed: %s\n",
241 s->fjfile->names + te->name_off, strerror(errno));
242 return 0;
245 static int fjfile_tab_cmpswap(fjfile_t s, unsigned i1, unsigned i2)
247 char* f1 = s->names + s->tab[i1].name_off;
248 char* f2 = s->names + s->tab[i2].name_off;
250 if (strcasecmp(f1, f2) > 0) {
251 if (s->options & FJFILE_VERBOSE) {
252 printf("Sort: 1: '%s' > 2: '%s', swapping\n", f1, f2);
253 fflush(stdout);
256 struct fjtabent t = s->tab[i1];
257 s->tab[i1] = s->tab[i2];
258 s->tab[i2] = t;
260 return 1;
261 } else
262 return 0;
265 int fjfile_sort_index(fjfile_t s)
267 int passes, swaps;
269 for (swaps = 0, passes = 0; passes < s->nent; passes++) {
270 int i, sw;
272 if (s->options & FJFILE_VERBOSE) {
273 printf("Sort: DOWN\n");
274 fflush(stdout);
277 for (sw = 0, i = 0; i < s->nent - 1; i++)
278 sw += fjfile_tab_cmpswap(s, i, i + 1);
279 swaps += sw;
280 if (!sw)
281 return swaps;
283 if (s->options & FJFILE_VERBOSE) {
284 printf("Sort: UP\n");
285 fflush(stdout);
288 for (sw = 0, i = s->nent - 1; i > 0; i--)
289 sw += fjfile_tab_cmpswap(s, i - 1, i);
290 swaps += sw;
291 if (!sw) {
292 return swaps;
296 fprintf(stderr, "Sort: pairwise comparison not transitive, aborting\n");
298 return -1;
301 int fjfile_write_header(fjfile_t s)
303 if (fseek(s->file, 0, SEEK_SET) < 0)
304 goto err;
305 if (fwrite(s->hdr, sizeof(*s->hdr), 1, s->file) != 1)
306 goto err;
307 if (fwrite(s->tab, sizeof(*s->tab), s->hdr->file_num, s->file) != s->hdr->file_num)
308 goto err;
309 if (fwrite(s->names, s->hdr->names_size, 1, s->file) != 1)
310 goto err;
312 return 0;
313 err:
314 fprintf(stderr, "Writing header tables failed: %s\n", strerror(errno));
315 return -1;
318 #define COPYBUF_SZ 4096
320 #define BACKUP_SUFFIX ".backup"
322 static int mkbackup(char* fname, int progress)
324 char backfname[PATH_MAX];
326 if (strlen(fname) > sizeof(backfname) - sizeof(BACKUP_SUFFIX))
327 return -1;
328 strcpy(backfname, fname);
329 strcat(backfname, BACKUP_SUFFIX);
331 FILE* backf = fopen(backfname, "rb");
332 FILE* f = 0;
333 if (!backf) {
334 f = fopen(fname, "rb");
335 if (!f)
336 goto err;
338 if (progress) {
339 printf("Creating backup: '%s'\n", backfname);
340 fflush(stdout);
343 backf = fopen(backfname, "wb");
344 if (!backf)
345 goto err;
347 size_t n, sz = 0;
348 char buf[COPYBUF_SZ];
349 while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
350 if (fwrite(buf, n, 1, backf) != 1)
351 goto err;
352 sz += n;
354 if (progress) {
355 printf("'%s': %d kb copied\n", backfname, sz >> 10);
356 fflush(stdout);
359 fclose(f);
362 fclose(backf);
363 return 0;
365 err:
366 if (backf)
367 fclose(backf);
368 if (f)
369 fclose(f);
370 fprintf(stderr, "Creating backup '%s' -> '%s' failed: %s\n",
371 fname, backfname, strerror(errno));
372 return -1;
375 static void list(fjfile_t fjf)
377 fjentry_t ent;
379 for (ent = fjfile_first_entry(fjf); ent; ent = fjentry_next(ent)) {
380 if (fjf->options & FJFILE_VERBOSE) {
381 printf("'%s' : name_off: %d, data_size: %d, data_off: 0x%X\n",
382 fjentry_name(ent), fjf->tab[ent->tabind].name_off,
383 fjentry_size(ent), fjf->tab[ent->tabind].data_off);
384 } else {
385 printf("%s %d\n", fjentry_name(ent), fjentry_size(ent));
389 fflush(stdout);
392 #define EXTRACT_SUFFIX ".d"
394 static char* format_destdir(const char* fjname, const char* destdir)
396 char* ddir;
398 if (!(destdir && *destdir)) {
399 const char* fname = strrchr(fjname, '/');
400 if (!fname)
401 fname = fjname;
402 else
403 fname++;
405 ddir = malloc(strlen(fname) + sizeof(EXTRACT_SUFFIX));
406 sprintf(ddir, "%s" EXTRACT_SUFFIX, fname);
407 } else {
408 size_t pl = strlen(destdir);
409 ddir = malloc(pl + 1);
410 strcpy(ddir, destdir);
411 while (--pl && ddir[pl] == '/')
412 ddir[pl] = '\0';
415 return ddir;
418 static int mkextractdir(const char* ddir)
420 struct stat dstat;
421 if (stat(ddir, &dstat) < 0) {
422 if (mkdir(ddir, 0777) < 0) {
423 fprintf(stderr, "Can't create destination directory '%s': %s\n",
424 ddir, strerror(errno));
425 return -1;
427 } else {
428 if (!S_ISDIR(dstat.st_mode)) {
429 fprintf(stderr, "Not a directory: '%s'\n", ddir);
430 return -1;
434 return 0;
437 static int entry_extract(fjentry_t s, const char* fname)
439 FILE* f = fopen(fname, "wb");
440 if (!f)
441 goto err;
443 printf("Extracting '%s' to '%s', size %d bytes\n",
444 fjentry_name(s), fname, fjentry_size(s));
445 fflush(stdout);
447 fjentry_seek(s, 0);
449 size_t n, sz = 0;
450 char buf[COPYBUF_SZ];
451 while ((n = fjentry_read(s, buf, sizeof(buf))) > 0) {
452 if (fwrite(buf, n, 1, f) != 1)
453 goto err;
454 sz += n;
457 fclose(f);
459 if (sz != fjentry_size(s)) {
460 fprintf(stderr, "Truncated file '%s': %d of %d bytes written\n",
461 fjentry_name(s), sz, fjentry_size(s));
462 return -1;
465 return 0;
466 err:
467 if (f)
468 fclose(f);
469 fprintf(stderr, "Can't extract to file '%s': %s\n", fname, strerror(errno));
470 return -1;
473 #define MGDHDR_SZ 0x60
474 #define MGD_MAGIC "MGD "
476 struct mgdhdr
478 char magic[4];
479 char padding[MGDHDR_SZ - 8];
480 unsigned png_size;
483 #define PNG_MAGIC "\x89PNG\r\n\x1A\n"
485 static int filter_mgd_test(fjentry_t s)
487 struct mgdhdr hdr;
488 unsigned sz;
490 sz = fjentry_read(s, &hdr, sizeof(hdr));
491 if (sz != sizeof(hdr))
492 return 0;
494 if (strncmp(hdr.magic, MGD_MAGIC, sizeof(hdr.magic)))
495 return 0;
497 char magic[sizeof(PNG_MAGIC) - 1];
498 sz = fjentry_read(s, magic, sizeof(magic));
499 if (strncmp(magic, PNG_MAGIC, sizeof(magic)))
500 return 0;
502 return 1;
505 static int entry_mgd_extract(fjentry_t s, const char* fname)
507 FILE* f = fopen(fname, "wb");
508 if (!f)
509 goto err;
511 struct mgdhdr hdr;
512 fjentry_seek(s, 0);
513 fjentry_read(s, &hdr, sizeof(hdr));
515 printf("PNG extracting '%s' to '%s', size %d bytes\n",
516 fjentry_name(s), fname, hdr.png_size);
517 fflush(stdout);
519 size_t n, sz = 0;
520 char buf[COPYBUF_SZ];
521 while ((n = fjentry_read(s, buf, sizeof(buf))) > 0) {
522 if (sz + n > hdr.png_size)
523 n = hdr.png_size - sz;
524 if (!n)
525 break;
526 if (fwrite(buf, n, 1, f) != 1)
527 goto err;
528 sz += n;
531 fclose(f);
533 if (sz != hdr.png_size) {
534 fprintf(stderr, "PNG extract truncated file '%s': %d of %d bytes written\n",
535 fjentry_name(s), sz, fjentry_size(s));
536 return -1;
539 return 0;
540 err:
541 if (f)
542 fclose(f);
543 fprintf(stderr, "Can't extract PNG to file '%s': %s\n", fname, strerror(errno));
544 return -1;
547 enum {EX_FILTER_MGD = 0x1};
549 static int extract(fjfile_t fjf, const char* destdir, unsigned filters)
551 char fname[PATH_MAX];
552 char* ddir = format_destdir(fjfile_name(fjf), destdir);
554 if (mkextractdir(ddir) < 0) {
555 free(ddir);
556 return -1;
559 fjentry_t ent;
560 for (ent = fjfile_first_entry(fjf); ent; ent = fjentry_next(ent)) {
561 if ((filters & EX_FILTER_MGD) && filter_mgd_test(ent)) {
562 int l = sprintf(fname, "%s/%s.png", ddir, fjentry_name(ent));
563 char* ext = fname + l - (sizeof(".mgd.png") - 1);
564 if (ext > fname && !strcasecmp(ext, ".mgd.png"))
565 strcpy(ext, ".png");
566 entry_mgd_extract(ent, fname);
567 } else {
568 sprintf(fname, "%s/%s", ddir, fjentry_name(ent));
569 entry_extract(ent, fname);
573 free(ddir);
574 return 0;
577 static const char* usage =
578 "Usage: fjfix [-fhltv] FILE [DESTDIR]\n"
579 "Fix entry index of, extract or create FJSYS (FileJoin) archive FILE.\n"
580 "Options:\n"
581 " -b - NO backup, otherwise creates FILE.backup before modification.\n"
582 " -f - fix entry index sort order to match system string collation order.\n"
583 " Fixes 'file not found' errors for broken collation order dependent software in WINE.\n"
584 " Uses WINE (unicode.org) collation order when run in WINE,\n"
585 " Windows (yet another non-standard) when run in Windows.\n"
586 " Native unix version uses POSIX (ASCII code) order, incompatible with both of above.\n"
587 " -h - this help.\n"
588 " -l - archive entry list with byte sizes.\n"
589 " -t - test entry index, do not fix it. Enabled by default.\n"
590 " -x - extract entries as files to DESTDIR or FILE.d if DESDIR not specified.\n"
591 " -v - verbose. Print additional data which might be useful for problem analysis and reporting.\n"
592 " -G - additionally extract PNG files embedded in MGD container entries. Must be used with -x.\n"
593 "Version " VERSION " built " __DATE__ "\n";
595 enum {
596 OPT_TEST = 0x1, OPT_LIST = 0x2, OPT_FIX = 0x4, OPT_BACKUP = 0x8,
597 OPT_EXTRACT = 0x10,
598 OPT_VERBOSE = 0x1000
601 int main(int argc, char* argv[0])
603 int opt;
604 unsigned options = OPT_TEST | OPT_BACKUP;
605 unsigned ext_filters = 0;
606 int needfix = 0;
607 char* fname;
609 while ((opt = getopt(argc, argv, "Gblvtfhx")) != -1) {
610 switch (opt) {
611 case 'G':
612 ext_filters |= EX_FILTER_MGD;
613 break;
614 case 'b':
615 options &= ~OPT_BACKUP;
616 break;
617 case 'f':
618 options |= OPT_FIX;
619 break;
620 case 'l':
621 options |= OPT_LIST;
622 break;
623 case 't':
624 options |= OPT_TEST;
625 break;
626 case 'x':
627 options |= OPT_EXTRACT;
628 break;
629 case 'v':
630 options |= OPT_VERBOSE;
631 break;
632 case 'h':
633 fprintf(stdout, "%s", usage);
634 return 0;
635 case '?':
636 default:
637 fprintf(stderr, "Try -h for options info.\n");
638 return 1;
642 if (optind >= argc) {
643 fprintf(stderr, "FILE argument required\n");
644 fprintf(stderr, "Try -h for usage info.\n");
645 return 1;
648 fname = argv[optind];
650 fjfile_t fjf = fjfile_open(fname);
651 if (!fjf) {
652 fprintf(stderr, "Reading '%s' failed\n", fname);
653 return 2;
656 if (options & OPT_VERBOSE) {
657 fjfile_set_options(fjf, FJFILE_VERBOSE);
658 printf("HEADER: file_num: %d, names_size: %d, data_off: 0x%X\n",
659 fjf->hdr->file_num, fjf->hdr->names_size, fjf->hdr->data_off);
660 fflush(stdout);
663 if (options & OPT_LIST) {
664 list(fjf);
667 if (options & OPT_EXTRACT) {
668 char* destdir = 0;
670 if (optind + 1 < argc)
671 destdir = argv[optind + 1];
673 extract(fjf, destdir, ext_filters);
676 if (options & (OPT_TEST | OPT_FIX)) {
677 int r = fjfile_sort_index(fjf);
678 if (r < 0)
679 return 3;
681 needfix = r > 0;
684 if (needfix) {
685 fprintf(stderr, "File index needs rebuilding\n");
688 if (needfix && (options & OPT_FIX)) {
689 if (options & OPT_BACKUP) {
690 if (mkbackup(fname, 1) < 0)
691 return 4;
693 printf("Updating index\n");
694 fflush(stdout);
696 if (fjfile_write_header(fjf) < 0)
697 return 5;
700 return 0;