cvt_data: allow fastcall compatible funcs
[ia32rtools.git] / tools / cvt_data.c
... / ...
CommitLineData
1/*
2 * ia32rtools
3 * (C) notaz, 2013,2014
4 *
5 * This work is licensed under the terms of 3-clause BSD license.
6 * See COPYING file in the top-level directory.
7 */
8
9#define _GNU_SOURCE
10#include <stdio.h>
11#include <stdlib.h>
12#include <string.h>
13#include <stdint.h>
14#include <inttypes.h>
15
16#include "my_assert.h"
17#include "my_str.h"
18#include "common.h"
19
20#include "protoparse.h"
21
22static const char *asmfn;
23static int asmln;
24
25static const struct parsed_proto *g_func_sym_pp;
26static char g_comment[256];
27static int g_warn_cnt;
28static int g_cconv_novalidate;
29static int g_arm_mode;
30
31// note: must be in ascending order
32enum dx_type {
33 DXT_UNSPEC,
34 DXT_BYTE,
35 DXT_WORD,
36 DXT_DWORD,
37 DXT_QUAD,
38 DXT_TEN,
39};
40
41#define anote(fmt, ...) \
42 printf("%s:%d: note: " fmt, asmfn, asmln, ##__VA_ARGS__)
43#define awarn(fmt, ...) do { \
44 printf("%s:%d: warning: " fmt, asmfn, asmln, ##__VA_ARGS__); \
45 if (++g_warn_cnt == 10) { \
46 fcloseall(); \
47 exit(1); \
48 } \
49} while (0)
50#define aerr(fmt, ...) do { \
51 printf("%s:%d: error: " fmt, asmfn, asmln, ##__VA_ARGS__); \
52 fcloseall(); \
53 exit(1); \
54} while (0)
55
56#include "masm_tools.h"
57
58static char *next_word_s(char *w, size_t wsize, char *s)
59{
60 int quote = 0;
61 size_t i;
62
63 s = sskip(s);
64
65 for (i = 0; i < wsize - 1; i++) {
66 if (s[i] == '\'')
67 quote ^= 1;
68 if (s[i] == 0 || (!quote && (my_isblank(s[i]) || s[i] == ',')))
69 break;
70 w[i] = s[i];
71 }
72 w[i] = 0;
73
74 if (s[i] != 0 && !my_isblank(s[i]) && s[i] != ',')
75 printf("warning: '%s' truncated\n", w);
76
77 return s + i;
78}
79
80static void next_section(FILE *fasm, char *name)
81{
82 char words[2][256];
83 char line[256];
84 int wordc;
85 char *p;
86
87 name[0] = 0;
88
89 while (my_fgets(line, sizeof(line), fasm))
90 {
91 wordc = 0;
92 asmln++;
93
94 p = sskip(line);
95 if (*p == 0)
96 continue;
97
98 if (*p == ';')
99 continue;
100
101 for (wordc = 0; wordc < ARRAY_SIZE(words); wordc++) {
102 p = sskip(next_word(words[wordc], sizeof(words[0]), p));
103 if (*p == 0 || *p == ';') {
104 wordc++;
105 break;
106 }
107 }
108
109 if (wordc < 2)
110 continue;
111
112 if (!IS(words[1], "segment"))
113 continue;
114
115 strcpy(name, words[0]);
116 break;
117 }
118}
119
120static enum dx_type parse_dx_directive(const char *name)
121{
122 if (IS(name, "dd"))
123 return DXT_DWORD;
124 if (IS(name, "dw"))
125 return DXT_WORD;
126 if (IS(name, "db"))
127 return DXT_BYTE;
128 if (IS(name, "dq"))
129 return DXT_QUAD;
130 if (IS(name, "dt"))
131 return DXT_TEN;
132
133 return DXT_UNSPEC;
134}
135
136static const char *type_name(enum dx_type type)
137{
138 switch (type) {
139 case DXT_BYTE:
140 return ".byte";
141 case DXT_WORD:
142 return ".hword";
143 case DXT_DWORD:
144 return ".long";
145 case DXT_QUAD:
146 return ".quad";
147 case DXT_TEN:
148 return ".tfloat";
149 case DXT_UNSPEC:
150 break;
151 }
152 return "<bad>";
153}
154
155static const char *type_name_float(enum dx_type type)
156{
157 switch (type) {
158 case DXT_DWORD:
159 return ".float";
160 case DXT_QUAD:
161 return ".double";
162 case DXT_TEN:
163 return ".tfloat";
164 default:
165 break;
166 }
167 return "<bad_float>";
168}
169
170static int type_size(enum dx_type type)
171{
172 switch (type) {
173 case DXT_BYTE:
174 return 1;
175 case DXT_WORD:
176 return 2;
177 case DXT_DWORD:
178 return 4;
179 case DXT_QUAD:
180 return 8;
181 case DXT_TEN:
182 return 10;
183 case DXT_UNSPEC:
184 break;
185 }
186 return -1;
187}
188
189static char *escape_string(char *s)
190{
191 char buf[256];
192 char *t = buf;
193
194 for (; *s != 0; s++) {
195 if (*s == '"') {
196 strcpy(t, "\\\"");
197 t += strlen(t);
198 continue;
199 }
200 if (*s == '\\') {
201 strcpy(t, "\\\\");
202 t += strlen(t);
203 continue;
204 }
205 *t++ = *s;
206 }
207 *t++ = *s;
208 if (t - buf > sizeof(buf))
209 aerr("string is too long\n");
210 return strcpy(s, buf);
211}
212
213static void sprint_pp_short(const struct parsed_proto *pp, char *buf,
214 size_t buf_size)
215{
216 char *p = buf;
217 size_t l;
218 int i;
219
220 if (pp->ret_type.is_ptr)
221 *p++ = 'p';
222 else if (IS(pp->ret_type.name, "void"))
223 *p++ = 'v';
224 else
225 *p++ = 'i';
226 *p++ = '(';
227 l = 2;
228
229 for (i = 0; i < pp->argc; i++) {
230 if (pp->arg[i].reg != NULL)
231 snprintf(buf + l, buf_size - l, "%s%s",
232 i == 0 ? "" : ",", pp->arg[i].reg);
233 else
234 snprintf(buf + l, buf_size - l, "%sa%d",
235 i == 0 ? "" : ",", i + 1);
236 l = strlen(buf);
237 }
238 snprintf(buf + l, buf_size - l, ")");
239}
240
241static const struct parsed_proto *check_var(FILE *fhdr,
242 const char *sym, const char *varname)
243{
244 const struct parsed_proto *pp, *pp_sym;
245 char fp_sym[256], fp_var[256], *p;
246 int i;
247
248 pp = proto_parse(fhdr, varname, 1);
249 if (pp == NULL) {
250 if (IS_START(varname, "sub_"))
251 awarn("sub_ sym missing proto: '%s'\n", varname);
252 return NULL;
253 }
254
255 if (!pp->is_func && !pp->is_fptr)
256 return NULL;
257
258 pp_print(fp_var, sizeof(fp_var), pp);
259
260 if (pp->argc_reg == 0)
261 goto check_sym;
262 if (pp->argc_reg == 1 && pp->argc_stack == 0
263 && IS(pp->arg[0].reg, "ecx"))
264 {
265 goto check_sym;
266 }
267 if (!g_cconv_novalidate
268 && (pp->argc_reg != 2
269 || !IS(pp->arg[0].reg, "ecx")
270 || !IS(pp->arg[1].reg, "edx")))
271 {
272 awarn("unhandled reg call: %s\n", fp_var);
273 }
274
275check_sym:
276 // fptrs must use 32bit args, callsite might have no information and
277 // lack a cast to smaller types, which results in incorrectly masked
278 // args passed (callee may assume masked args, it does on ARM)
279 for (i = 0; i < pp->argc; i++) {
280 if (pp->arg[i].type.is_ptr)
281 continue;
282 p = pp->arg[i].type.name;
283 if (strstr(p, "int8") || strstr(p, "int16")
284 || strstr(p, "char") || strstr(p, "short"))
285 {
286 awarn("reference to %s with arg%d '%s'\n", pp->name, i + 1, p);
287 }
288 }
289
290 sprint_pp_short(pp, g_comment, sizeof(g_comment));
291
292 if (sym != NULL) {
293 g_func_sym_pp = NULL;
294 pp_sym = proto_parse(fhdr, sym, 1);
295 if (pp_sym == NULL)
296 return pp;
297 if (!pp_sym->is_fptr)
298 aerr("func ptr data, but label '%s' !is_fptr\n", pp_sym->name);
299 g_func_sym_pp = pp_sym;
300 }
301 else {
302 pp_sym = g_func_sym_pp;
303 if (pp_sym == NULL)
304 return pp;
305 }
306
307 if (pp_cmp_func(pp, pp_sym)) {
308 if (pp_sym->argc_stack == 0 && pp_sym->is_fastcall
309 && pp->argc_stack == 0
310 && (pp->is_fastcall || pp->argc_reg == 0)
311 && pp_sym->argc_reg > pp->argc_reg)
312 ; /* fascall compatible func doesn't use all args -> ok */
313 else {
314 pp_print(fp_sym, sizeof(fp_sym), pp_sym);
315 anote("var: %s\n", fp_var);
316 anote("sym: %s\n", fp_sym);
317 awarn("^ mismatch\n");
318 }
319 }
320
321 return pp;
322}
323
324static void output_decorated_pp(FILE *fout,
325 const struct parsed_proto *pp)
326{
327 if (pp->name[0] != '_')
328 fprintf(fout, pp->is_fastcall ? "@" : "_");
329 fprintf(fout, "%s", pp->name);
330 if (pp->is_stdcall && pp->argc > 0)
331 fprintf(fout, "@%d", pp->argc * 4);
332}
333
334static int align_value(int src_val)
335{
336 if (src_val <= 0) {
337 awarn("bad align: %d\n", src_val);
338 src_val = 1;
339 }
340 if (!g_arm_mode)
341 return src_val;
342
343 return __builtin_ffs(src_val) - 1;
344}
345
346static int cmpstringp(const void *p1, const void *p2)
347{
348 return strcmp(*(char * const *)p1, *(char * const *)p2);
349}
350
351/* XXX: maybe move to external file? */
352static const char *unwanted_syms[] = {
353 "aRuntimeError",
354 "aTlossError",
355 "aSingError",
356 "aDomainError",
357 "aR6029ThisAppli",
358 "aR6028UnableToI",
359 "aR6027NotEnough",
360 "aR6026NotEnough",
361 "aR6025PureVirtu",
362 "aR6024NotEnough",
363 "aR6019UnableToO",
364 "aR6018Unexpecte",
365 "aR6017Unexpecte",
366 "aR6016NotEnough",
367 "aAbnormalProgra",
368 "aR6009NotEnough",
369 "aR6008NotEnough",
370 "aR6002FloatingP",
371 "aMicrosoftVisua",
372 "aRuntimeErrorPr",
373 "aThisApplicatio",
374 "aMicrosoftFindF",
375 "aMicrosoftOffic",
376};
377
378static int is_unwanted_sym(const char *sym)
379{
380 return bsearch(&sym, unwanted_syms, ARRAY_SIZE(unwanted_syms),
381 sizeof(unwanted_syms[0]), cmpstringp) != NULL;
382}
383
384int main(int argc, char *argv[])
385{
386 FILE *fout, *fasm, *fhdr = NULL, *frlist;
387 const struct parsed_proto *pp;
388 int no_decorations = 0;
389 char comment_char = '#';
390 char words[20][256];
391 char word[256];
392 char line[256];
393 char last_sym[32];
394 unsigned long val;
395 unsigned long cnt;
396 uint64_t val64;
397 const char *sym;
398 enum dx_type type;
399 char **pub_syms;
400 int pub_sym_cnt = 0;
401 int pub_sym_alloc;
402 char **rlist;
403 int rlist_cnt = 0;
404 int rlist_alloc;
405 int header_mode = 0;
406 int is_ro = 0;
407 int is_label;
408 int is_bss;
409 int wordc;
410 int first;
411 int arg_out;
412 int arg = 1;
413 int len;
414 int w, i;
415 char *p;
416 char *p2;
417
418 if (argc < 4) {
419 // -nd: no symbol decorations
420 printf("usage:\n%s [-nd] [-i] [-a] <.s> <.asm> <hdrf> [rlist]*\n"
421 "%s -hdr <.h> <.asm>\n",
422 argv[0], argv[0]);
423 return 1;
424 }
425
426 for (arg = 1; arg < argc; arg++) {
427 if (IS(argv[arg], "-nd"))
428 no_decorations = 1;
429 else if (IS(argv[arg], "-i"))
430 g_cconv_novalidate = 1;
431 else if (IS(argv[arg], "-a")) {
432 comment_char = '@';
433 g_arm_mode = 1;
434 }
435 else if (IS(argv[arg], "-hdr"))
436 header_mode = 1;
437 else
438 break;
439 }
440
441 arg_out = arg++;
442
443 asmfn = argv[arg++];
444 fasm = fopen(asmfn, "r");
445 my_assert_not(fasm, NULL);
446
447 if (!header_mode) {
448 hdrfn = argv[arg++];
449 fhdr = fopen(hdrfn, "r");
450 my_assert_not(fhdr, NULL);
451 }
452
453 fout = fopen(argv[arg_out], "w");
454 my_assert_not(fout, NULL);
455
456 pub_sym_alloc = 64;
457 pub_syms = malloc(pub_sym_alloc * sizeof(pub_syms[0]));
458 my_assert_not(pub_syms, NULL);
459
460 rlist_alloc = 64;
461 rlist = malloc(rlist_alloc * sizeof(rlist[0]));
462 my_assert_not(rlist, NULL);
463
464 for (; arg < argc; arg++) {
465 frlist = fopen(argv[arg], "r");
466 my_assert_not(frlist, NULL);
467
468 while (my_fgets(line, sizeof(line), frlist)) {
469 p = sskip(line);
470 if (*p == 0 || *p == ';')
471 continue;
472
473 p = next_word(words[0], sizeof(words[0]), p);
474 if (words[0][0] == 0)
475 continue;
476
477 if (rlist_cnt >= rlist_alloc) {
478 rlist_alloc = rlist_alloc * 2 + 64;
479 rlist = realloc(rlist, rlist_alloc * sizeof(rlist[0]));
480 my_assert_not(rlist, NULL);
481 }
482 rlist[rlist_cnt++] = strdup(words[0]);
483 }
484
485 fclose(frlist);
486 frlist = NULL;
487 }
488
489 if (rlist_cnt > 0)
490 qsort(rlist, rlist_cnt, sizeof(rlist[0]), cmpstringp);
491
492 qsort(unwanted_syms, ARRAY_SIZE(unwanted_syms),
493 sizeof(unwanted_syms[0]), cmpstringp);
494
495 last_sym[0] = 0;
496
497 while (1) {
498 next_section(fasm, line);
499 if (feof(fasm))
500 break;
501 if (IS(line + 1, "text"))
502 continue;
503
504 if (IS(line + 1, "rdata")) {
505 is_ro = 1;
506 if (!header_mode)
507 fprintf(fout, "\n.section .rodata\n");
508 }
509 else if (IS(line + 1, "data")) {
510 is_ro = 0;
511 if (!header_mode)
512 fprintf(fout, "\n.data\n");
513 }
514 else
515 aerr("unhandled section: '%s'\n", line);
516
517 if (!header_mode)
518 fprintf(fout, ".align %d\n", align_value(4));
519
520 while (my_fgets(line, sizeof(line), fasm))
521 {
522 sym = NULL;
523 asmln++;
524
525 p = sskip(line);
526 if (*p == 0)
527 continue;
528
529 if (*p == ';') {
530 if (IS_START(p, ";org") && sscanf(p + 5, "%Xh", &i) == 1) {
531 // ;org is only seen at section start, so assume . addr 0
532 i &= 0xfff;
533 if (i != 0 && !header_mode)
534 fprintf(fout, "\t\t .skip 0x%x\n", i);
535 }
536 continue;
537 }
538
539 for (wordc = 0; wordc < ARRAY_SIZE(words); wordc++) {
540 p = sskip(next_word_s(words[wordc], sizeof(words[0]), p));
541 if (*p == 0 || *p == ';') {
542 wordc++;
543 break;
544 }
545 if (*p == ',') {
546 p = sskip(p + 1);
547 }
548 }
549
550 if (*p == ';') {
551 p = sskip(p + 1);
552 if (IS_START(p, "sctclrtype"))
553 g_func_sym_pp = NULL;
554 }
555
556 if (wordc == 2 && IS(words[1], "ends"))
557 break;
558 if (wordc <= 2 && IS(words[0], "end"))
559 break;
560 if (wordc < 2)
561 aerr("unhandled: '%s'\n", words[0]);
562
563 // don't cares
564 if (IS(words[0], "assume"))
565 continue;
566
567 if (IS(words[0], "align")) {
568 if (header_mode)
569 continue;
570
571 val = parse_number(words[1], 0);
572 fprintf(fout, "\t\t .align %d", align_value(val));
573 goto fin;
574 }
575
576 if (IS(words[0], "public")) {
577 // skip, sym should appear in header anyway
578 continue;
579 }
580
581 w = 1;
582 type = parse_dx_directive(words[0]);
583 if (type == DXT_UNSPEC) {
584 type = parse_dx_directive(words[1]);
585 sym = words[0];
586 w = 2;
587 }
588 if (type == DXT_UNSPEC)
589 aerr("unhandled decl: '%s %s'\n", words[0], words[1]);
590
591 if (sym != NULL)
592 {
593 if (header_mode) {
594 int is_str = 0;
595
596 fprintf(fout, "extern ");
597 if (is_ro)
598 fprintf(fout, "const ");
599
600 switch (type) {
601 case DXT_BYTE:
602 for (i = w; i < wordc; i++)
603 if (words[i][0] == '\'')
604 is_str = 1;
605 if (is_str)
606 fprintf(fout, "char %s[];\n", sym);
607 else
608 fprintf(fout, "uint8_t %s;\n", sym);
609 break;
610
611 case DXT_WORD:
612 fprintf(fout, "uint16_t %s;\n", sym);
613 break;
614
615 case DXT_DWORD:
616 fprintf(fout, "uint32_t %s;\n", sym);
617 break;
618
619 default:
620 fprintf(fout, "_UNKNOWN %s;\n", sym);
621 break;
622 }
623
624 continue;
625 }
626
627 snprintf(last_sym, sizeof(last_sym), "%s", sym);
628
629 pp = proto_parse(fhdr, sym, 1);
630 if (pp != NULL) {
631 g_func_sym_pp = NULL;
632
633 // public/global name
634 if (pub_sym_cnt >= pub_sym_alloc) {
635 pub_sym_alloc *= 2;
636 pub_syms = realloc(pub_syms, pub_sym_alloc * sizeof(pub_syms[0]));
637 my_assert_not(pub_syms, NULL);
638 }
639 pub_syms[pub_sym_cnt++] = strdup(sym);
640 }
641
642 len = strlen(sym);
643 fprintf(fout, "%s%s:", no_decorations ? "" : "_", sym);
644
645 len += 2;
646 if (len < 8)
647 fprintf(fout, "\t");
648 if (len < 16)
649 fprintf(fout, "\t");
650 if (len <= 16)
651 fprintf(fout, " ");
652 else
653 fprintf(fout, " ");
654 }
655 else {
656 if (header_mode)
657 continue;
658
659 fprintf(fout, "\t\t ");
660 }
661
662 // fill out some unwanted strings with zeroes..
663 if (type == DXT_BYTE && words[w][0] == '\''
664 && is_unwanted_sym(last_sym))
665 {
666 len = 0;
667 for (; w < wordc; w++) {
668 if (words[w][0] == '\'') {
669 p = words[w] + 1;
670 for (; *p && *p != '\''; p++)
671 len++;
672 }
673 else {
674 // assume encoded byte
675 len++;
676 }
677 }
678 fprintf(fout, ".skip %d", len);
679 goto fin;
680 }
681 else if (type == DXT_BYTE
682 && (words[w][0] == '\''
683 || (w + 1 < wordc && words[w + 1][0] == '\'')))
684 {
685 // string; use asciz for most common case
686 if (w == wordc - 2 && IS(words[w + 1], "0")) {
687 fprintf(fout, ".asciz \"");
688 wordc--;
689 }
690 else
691 fprintf(fout, ".ascii \"");
692
693 for (; w < wordc; w++) {
694 if (words[w][0] == '\'') {
695 p = words[w] + 1;
696 p2 = strchr(p, '\'');
697 if (p2 == NULL)
698 aerr("unterminated string? '%s'\n", p);
699 memcpy(word, p, p2 - p);
700 word[p2 - p] = 0;
701 fprintf(fout, "%s", escape_string(word));
702 }
703 else {
704 val = parse_number(words[w], 0);
705 if (val & ~0xff)
706 aerr("bad string trailing byte?\n");
707 // unfortunately \xHH is unusable - gas interprets
708 // things like \x27b as 0x7b, so have to use octal here
709 fprintf(fout, "\\%03lo", val);
710 }
711 }
712 fprintf(fout, "\"");
713 goto fin;
714 }
715
716 if (w == wordc - 2) {
717 if (IS_START(words[w + 1], "dup(")) {
718 cnt = parse_number(words[w], 0);
719 p = words[w + 1] + 4;
720 p2 = strchr(p, ')');
721 if (p2 == NULL)
722 aerr("bad dup?\n");
723 memmove(word, p, p2 - p);
724 word[p2 - p] = 0;
725
726 val = 0;
727 if (!IS(word, "?"))
728 val = parse_number(word, 0);
729
730 fprintf(fout, ".fill 0x%02lx,%d,0x%02lx",
731 cnt, type_size(type), val);
732 goto fin;
733 }
734 }
735
736 if (type == DXT_DWORD && words[w][0] == '\''
737 && words[w][5] == '\'' && strlen(words[w]) == 6)
738 {
739 if (w != wordc - 1)
740 aerr("TODO\n");
741
742 p = words[w];
743 val = (p[1] << 24) | (p[2] << 16) | (p[3] << 8) | p[4];
744 fprintf(fout, ".long 0x%lx", val);
745 snprintf(g_comment, sizeof(g_comment), "%s", words[w]);
746 goto fin;
747 }
748
749 if (type >= DXT_DWORD && strchr(words[w], '.'))
750 {
751 if (w != wordc - 1)
752 aerr("TODO\n");
753
754 if (g_arm_mode && type == DXT_TEN) {
755 fprintf(fout, ".fill 10");
756 snprintf(g_comment, sizeof(g_comment), "%s %s",
757 type_name_float(type), words[w]);
758 }
759 else
760 fprintf(fout, "%s %s", type_name_float(type), words[w]);
761 goto fin;
762 }
763
764 first = 1;
765 fprintf(fout, "%s ", type_name(type));
766 for (; w < wordc; w++)
767 {
768 if (!first)
769 fprintf(fout, ", ");
770
771 is_label = is_bss = 0;
772 if (w <= wordc - 2 && IS(words[w], "offset")) {
773 is_label = 1;
774 w++;
775 }
776 else if (IS(words[w], "?")) {
777 is_bss = 1;
778 }
779 else if (type == DXT_DWORD
780 && !('0' <= words[w][0] && words[w][0] <= '9'))
781 {
782 // assume label
783 is_label = 1;
784 }
785
786 if (is_bss) {
787 fprintf(fout, "0");
788 }
789 else if (is_label) {
790 p = words[w];
791 if (IS_START(p, "loc_") || IS_START(p, "__imp")
792 || strchr(p, '?') || strchr(p, '@')
793 || bsearch(&p, rlist, rlist_cnt, sizeof(rlist[0]),
794 cmpstringp))
795 {
796 fprintf(fout, "0");
797 snprintf(g_comment, sizeof(g_comment), "%s", p);
798 }
799 else {
800 pp = check_var(fhdr, sym, p);
801 if (pp == NULL) {
802 fprintf(fout, "%s%s",
803 (no_decorations || p[0] == '_') ? "" : "_", p);
804 }
805 else {
806 if (no_decorations)
807 fprintf(fout, "%s", pp->name);
808 else
809 output_decorated_pp(fout, pp);
810 }
811 }
812 }
813 else {
814 val64 = parse_number(words[w], 1);
815 if (val64 < 10)
816 fprintf(fout, "%d", (int)val64);
817 else
818 fprintf(fout, "0x%" PRIx64, val64);
819 }
820
821 first = 0;
822 }
823
824fin:
825 if (g_comment[0] != 0) {
826 fprintf(fout, "\t\t%c %s", comment_char, g_comment);
827 g_comment[0] = 0;
828 }
829 fprintf(fout, "\n");
830 }
831 }
832
833 fprintf(fout, "\n");
834
835 // dump public syms
836 for (i = 0; i < pub_sym_cnt; i++)
837 fprintf(fout, ".global %s%s\n",
838 no_decorations ? "" : "_", pub_syms[i]);
839
840 fclose(fout);
841 fclose(fasm);
842 if (fhdr != NULL)
843 fclose(fhdr);
844
845 return 0;
846}
847
848// vim:ts=2:shiftwidth=2:expandtab