bde1e5a8c1455e2004917679961fb68b828eb34c
[projects/chimara/chimara.git] / babel / tads.c
1 /* \r
2  *   tads.c - Treaty of Babel common functions for tads2 and tads3 modules\r
3  *   \r
4  *   This file depends on treaty_builder.h\r
5  *   \r
6  *   This file is public domain, but note that any changes to this file may\r
7  *   render it noncompliant with the Treaty of Babel\r
8  *   \r
9  *   Modified\r
10  *.   04/08/2006 LRRaszewski - changed babel API calls to threadsafe versions\r
11  *.   04/08/2006 MJRoberts  - initial implementation\r
12  */\r
13 \r
14 \r
15 #include "treaty.h"\r
16 #include <ctype.h>\r
17 #include <stdio.h>\r
18 #include <string.h>\r
19 #include <stdlib.h>\r
20 #include "tads.h"\r
21 #include "md5.h"\r
22 \r
23 #define ASSERT_OUTPUT_SIZE(x) \\r
24     do { if (output_extent < (x)) return INVALID_USAGE_RV; } while (0)\r
25 \r
26 #define T2_SIGNATURE "TADS2 bin\012\015\032"\r
27 #define T3_SIGNATURE "T3-image\015\012\032"\r
28 \r
29 #ifndef FALSE\r
30 #define FALSE 0\r
31 #endif\r
32 #ifndef TRUE\r
33 #define TRUE 1\r
34 #endif\r
35 \r
36 /* ------------------------------------------------------------------------ */\r
37 /*\r
38  *   private structures \r
39  */\r
40 \r
41 /*\r
42  *   resource information structure - this encapsulates the location and size\r
43  *   of a binary resource object embedded in a story file \r
44  */\r
45 typedef struct resinfo resinfo;\r
46 struct resinfo\r
47 {\r
48     /* pointer and length of the data in the story file buffer */\r
49     const char *ptr;\r
50     int32 len;\r
51 \r
52     /* tads major version (currently, 2 or 3) */\r
53     int tads_version;\r
54 };\r
55 \r
56 /*\r
57  *   Name/value pair list entry \r
58  */\r
59 typedef struct valinfo valinfo;\r
60 struct valinfo\r
61 {\r
62     const char *name;\r
63     size_t name_len;\r
64 \r
65     /* value string */\r
66     char *val;\r
67     size_t val_len;\r
68 \r
69     /* next entry in the list */\r
70     valinfo *nxt;\r
71 };\r
72 \r
73 \r
74 /* ------------------------------------------------------------------------ */\r
75 /*\r
76  *   forward declarations \r
77  */\r
78 static valinfo *parse_game_info(const void *story_file, int32 story_len,\r
79                                 int *version);\r
80 static int find_resource(const void *story_file, int32 story_len,\r
81                          const char *resname, resinfo *info);\r
82 static int find_cover_art(const void *story_file, int32 story_len,\r
83                           resinfo *resp, int32 *image_format,\r
84                           int32 *width, int32 *height);\r
85 static int t2_find_res(const void *story_file, int32 story_len,\r
86                        const char *resname, resinfo *info);\r
87 static int t3_find_res(const void *story_file, int32 story_len,\r
88                        const char *resname, resinfo *info);\r
89 static valinfo *find_by_key(valinfo *list_head, const char *key);\r
90 static void delete_valinfo_list(valinfo *head);\r
91 static int32 generate_md5_ifid(void *story_file, int32 extent,\r
92                                char *output, int32 output_extent);\r
93 static int32 synth_ifiction(valinfo *vals, int tads_version,\r
94                             char *buf, int32 bufsize,\r
95                             void *story_file, int32 extent);\r
96 static int get_png_dim(const void *img, int32 extent,\r
97                        int32 *xout, int32 *yout);\r
98 static int get_jpeg_dim(const void *img, int32 extent,\r
99                         int32 *xout, int32 *yout);\r
100 \r
101 \r
102 \r
103 /* ------------------------------------------------------------------------ */\r
104 /*\r
105  *   Get the IFID for a given story file.  \r
106  */\r
107 int32 tads_get_story_file_IFID(void *story_file, int32 extent,\r
108                                char *output, int32 output_extent)\r
109 {\r
110     valinfo *vals;\r
111     \r
112     /* if we have GameInfo, try looking for an IFID there */\r
113     if ((vals = parse_game_info(story_file, extent, 0)) != 0)\r
114     {\r
115         valinfo *val;\r
116         int found = 0;\r
117         \r
118         /* find the "IFID" key */\r
119         if ((val = find_by_key(vals, "IFID")) != 0)\r
120         {\r
121             char *p;\r
122             \r
123             /* copy the output as a null-terminated string */\r
124             ASSERT_OUTPUT_SIZE((int32)val->val_len + 1);\r
125             memcpy(output, val->val, val->val_len);\r
126             output[val->val_len] = '\0';\r
127 \r
128             /* \r
129              *   count up the IFIDs in the buffer - there might be more than\r
130              *   one, separated by commas \r
131              */\r
132             for (found = 1, p = output ; *p != '\0' ; ++p)\r
133             {\r
134                 /* if this is a comma, it delimits a new IFID */\r
135                 if (*p == ',')\r
136                     ++found;\r
137             }\r
138         }\r
139 \r
140         /* delete the GameInfo list */\r
141         delete_valinfo_list(vals);\r
142 \r
143         /* if we found an IFID, indicate how many results we found */\r
144         if (found != 0)\r
145             return found;\r
146     }\r
147 \r
148     /* \r
149      *   we didn't find an IFID in the GameInfo, so generate a default IFID\r
150      *   using the MD5 method \r
151      */\r
152     return generate_md5_ifid(story_file, extent, output, output_extent);\r
153 }\r
154 \r
155 /*\r
156  *   Get the size of the ifiction metadata for the game \r
157  */\r
158 int32 tads_get_story_file_metadata_extent(void *story_file, int32 extent)\r
159 {\r
160     valinfo *vals;\r
161     int32 ret;\r
162     int ver;\r
163     \r
164     /*\r
165      *   First, make sure we have a GameInfo record.  If we don't, simply\r
166      *   indicate that there's no metadata to fetch.  \r
167      */\r
168     if ((vals = parse_game_info(story_file, extent, &ver)) == 0)\r
169         return NO_REPLY_RV;\r
170 \r
171     /*\r
172      *   Run the ifiction synthesizer with no output buffer, to calculate the\r
173      *   size we need. \r
174      */\r
175     ret = synth_ifiction(vals, ver, 0, 0, story_file, extent);\r
176 \r
177     /* delete the value list */\r
178     delete_valinfo_list(vals);\r
179 \r
180     /* return the required size */\r
181     return ret;\r
182 }\r
183 \r
184 /*\r
185  *   Get the ifiction metadata for the game\r
186  */\r
187 int32 tads_get_story_file_metadata(void *story_file, int32 extent,\r
188                                    char *buf, int32 bufsize)\r
189 {\r
190     valinfo *vals;\r
191     int32 ret;\r
192     int ver;\r
193 \r
194     /* make sure we have metadata to fetch */\r
195     if ((vals = parse_game_info(story_file, extent, &ver)) == 0)\r
196         return NO_REPLY_RV;\r
197 \r
198     /* synthesize the ifiction data into the output buffer */\r
199     ret = synth_ifiction(vals, ver, buf, bufsize, story_file, extent);\r
200 \r
201     /* if that required more space than we had available, return an error */\r
202     if (ret > bufsize)\r
203         ret = INVALID_USAGE_RV;\r
204 \r
205     /* delete the value list */\r
206     delete_valinfo_list(vals);\r
207 \r
208     /* return the result */\r
209     return ret;\r
210 }\r
211 \r
212 /*\r
213  *   Get the size of the cover art \r
214  */\r
215 int32 tads_get_story_file_cover_extent(void *story_file, int32 story_len)\r
216 {\r
217     resinfo res;\r
218     \r
219     /* look for the cover art resource */\r
220     if (find_cover_art(story_file, story_len, &res, 0, 0, 0))\r
221         return res.len;\r
222     else\r
223         return NO_REPLY_RV;\r
224 }\r
225 \r
226 /*\r
227  *   Get the format of the cover art \r
228  */\r
229 int32 tads_get_story_file_cover_format(void *story_file, int32 story_len)\r
230 {\r
231     int32 typ;\r
232 \r
233     /* look for CoverArt.jpg */\r
234     if (find_cover_art(story_file, story_len, 0, &typ, 0, 0))\r
235         return typ;\r
236     else\r
237         return NO_REPLY_RV;\r
238 }\r
239 \r
240 /*\r
241  *   Get the cover art data \r
242  */\r
243 int32 tads_get_story_file_cover(void *story_file, int32 story_len,\r
244                                 void *outbuf, int32 output_extent)\r
245 {\r
246     resinfo res;\r
247 \r
248     /* look for CoverArt.jpg, then for CoverArt.png */\r
249     if (find_cover_art(story_file, story_len, &res, 0, 0, 0))\r
250     {\r
251         /* got it - copy the data to the buffer */\r
252         ASSERT_OUTPUT_SIZE(res.len);\r
253         memcpy(outbuf, res.ptr, res.len);\r
254 \r
255         /* success */\r
256         return res.len;\r
257     }\r
258 \r
259     /* otherwise, we didn't find it */\r
260     return NO_REPLY_RV;\r
261 }\r
262 \r
263 /* ------------------------------------------------------------------------ */\r
264 /*\r
265  *   Generate a default IFID using the MD5 hash method \r
266  */\r
267 static int32 generate_md5_ifid(void *story_file, int32 extent,\r
268                                char *output, int32 output_extent)\r
269 {\r
270     md5_state_t md5;\r
271     unsigned char md5_buf[16];\r
272     char *p;\r
273     int i;\r
274 \r
275     /* calculate the MD5 hash of the story file */\r
276     md5_init(&md5);\r
277     md5_append(&md5, story_file, extent);\r
278     md5_finish(&md5, md5_buf);\r
279 \r
280     /* make sure we have room to store the result */\r
281     ASSERT_OUTPUT_SIZE(39);\r
282 \r
283     /* the prefix is "TADS2-" or "TADS3-", depending on the format */\r
284     if (tads_match_sig(story_file, extent, T2_SIGNATURE))\r
285         strcpy(output, "TADS2-");\r
286     else\r
287         strcpy(output, "TADS3-");\r
288 \r
289     /* the rest is the MD5 hash of the file, as hex digits */\r
290     for (i = 0, p = output + strlen(output) ; i < 16 ; p += 2, ++i)\r
291         sprintf(p, "%02X", md5_buf[i]);\r
292 \r
293     /* indicate that we found one result */\r
294     return 1;\r
295 }\r
296 \r
297 /* ------------------------------------------------------------------------ */\r
298 /*\r
299  *   Some UTF-8 utility functions and macros.  We use our own rather than the\r
300  *   ctype.h macros because we're parsing UTF-8 text.  \r
301  */\r
302 \r
303 /* is c a space? */\r
304 #define u_isspace(c) ((unsigned char)(c) < 128 && isspace(c))\r
305 \r
306 /* is c a horizontal space? */\r
307 #define u_ishspace(c) (u_isspace(c) && (c) != '\n' && (c) != '\r')\r
308 \r
309 /* is-newline - matches \n, \r, and \u2028 */\r
310 static int u_isnl(const char *p, int32 len)\r
311 {\r
312     return (*p == '\n' \r
313             || *p == '\r'\r
314             || (len >= 3\r
315                 && *(unsigned char *)p == 0xe2\r
316                 && *(unsigned char *)(p+1) == 0x80\r
317                 && *(unsigned char *)(p+2) == 0xa8));\r
318 }\r
319 \r
320 /* skip to the next utf-8 character */\r
321 static void nextc(const char **p, int32 *len)\r
322 {\r
323     /* skip the first byte */\r
324     if (*len != 0)\r
325         ++*p, --*len;\r
326 \r
327     /* skip continuation bytes */\r
328     while (*len != 0 && (**p & 0xC0) == 0x80)\r
329         ++*p, --*len;\r
330 }\r
331 \r
332 /* skip to the previous utf-8 character */\r
333 static void prevc(const char **p, int32 *len)\r
334 {\r
335     /* move back one byte */\r
336     --*p, ++*len;\r
337 \r
338     /* keep skipping as long as we're looking at continuation characters */\r
339     while ((**p & 0xC0) == 0x80)\r
340         --*p, ++*len;\r
341 }\r
342 \r
343 /*\r
344  *   Skip a newline sequence.  Skips all common conventions, including \n,\r
345  *   \r, \n\r, \r\n, and \u2028.  \r
346  */\r
347 static void skip_newline(const char **p, int32 *rem)\r
348 {\r
349     /* make sure we have something to skip */\r
350     if (*rem == 0)\r
351         return;\r
352 \r
353     /* check what we have */\r
354     switch (**(const unsigned char **)p)\r
355     {\r
356     case '\n':\r
357         /* skip \n or \n\r */\r
358         nextc(p, rem);\r
359         if (**p == '\r')\r
360             nextc(p, rem);\r
361         break;\r
362 \r
363     case '\r':\r
364         /* skip \r or \r\n */\r
365         nextc(p, rem);\r
366         if (**p == '\n')\r
367             nextc(p, rem);\r
368         break;\r
369 \r
370     case 0xe2:\r
371         /* \u2028 (unicode line separator) - just skip the one character */\r
372         nextc(p, rem);\r
373         break;\r
374     }\r
375 }\r
376 \r
377 /*\r
378  *   Skip to the next line \r
379  */\r
380 static void skip_to_next_line(const char **p, int32 *rem)\r
381 {\r
382     /* look for the next newline */\r
383     for ( ; *rem != 0 ; nextc(p, rem))\r
384     {\r
385         /* if this is a newline of some kind, we're at the end of the line */\r
386         if (u_isnl(*p, *rem))\r
387         {\r
388             /* skip the newline, and we're done */\r
389             skip_newline(p, rem);\r
390             break;\r
391         }\r
392     }\r
393 }\r
394 \r
395 \r
396 /* ------------------------------------------------------------------------ */\r
397 /*\r
398  *   ifiction synthesizer output context \r
399  */\r
400 typedef struct synthctx synthctx;\r
401 struct synthctx\r
402 {\r
403     /* the current output pointer */\r
404     char *buf;\r
405 \r
406     /* the number of bytes remaining in the output buffer */\r
407     int32 buf_size;\r
408 \r
409     /* \r
410      *   the total number of bytes needed for the output (this might be more\r
411      *   than we've actually written, since we count up the bytes required\r
412      *   even if we need more space than the buffer provides) \r
413      */\r
414     int32 total_size;\r
415 \r
416     /* the head of the name/value pair list from the parsed GameInfo */\r
417     valinfo *vals;\r
418 };\r
419 \r
420 /* initialize a synthesizer context */\r
421 static void init_synthctx(synthctx *ctx, char *buf, int32 bufsize,\r
422                           valinfo *vals)\r
423 {\r
424     /* set up at the beginning of the output buffer */\r
425     ctx->buf = buf;\r
426     ctx->buf_size = bufsize;\r
427 \r
428     /* we haven't written anything to the output buffer yet */\r
429     ctx->total_size = 0;\r
430 \r
431     /* remember the name/value pair list */\r
432     ctx->vals = vals;\r
433 }\r
434 \r
435 /* \r
436  *   Write out a chunk to a synthesized ifiction record, updating pointers\r
437  *   and counters.  We won't copy past the end of the buffer, but we'll\r
438  *   continue counting the output length needed in any case.  \r
439  */\r
440 static void write_ifiction(synthctx *ctx, const char *src, size_t srclen)\r
441 {\r
442     int32 copy_len;\r
443 \r
444     /* copy as much as we can, up to the remaining buffer size */\r
445     copy_len = srclen;\r
446     if (copy_len > ctx->buf_size)\r
447         copy_len = ctx->buf_size;\r
448 \r
449     /* do the copying, if any */\r
450     if (copy_len != 0)\r
451     {\r
452         /* copy the bytes */\r
453         memcpy(ctx->buf, src, (size_t)copy_len);\r
454 \r
455         /* adjust the buffer pointer and output buffer size remaining */\r
456         ctx->buf += copy_len;\r
457         ctx->buf_size -= copy_len;\r
458     }\r
459 \r
460     /* count this source data in the total size */\r
461     ctx->total_size += srclen;\r
462 }\r
463 \r
464 /* write a null-terminated chunk to the synthesized ifiction record */\r
465 static void write_ifiction_z(synthctx *ctx, const char *src)\r
466 {\r
467     write_ifiction(ctx, src, strlen(src));\r
468 }\r
469 \r
470 /*\r
471  *   Write a PCDATA string to the synthesized ifiction record.  In\r
472  *   particular, we rewrite '<', '>', and '&' as '&lt;', '&gt;', and '&amp;',\r
473  *   respectively; we trim off leading and trailing spaces; and we compress\r
474  *   each run of whitespace down to a single \u0020 (' ') character.\r
475  */\r
476 static void write_ifiction_pcdata(synthctx *ctx, const char *p, size_t len)\r
477 {\r
478     /* first, skip any leading whitespace */\r
479     for ( ; len != 0 && u_ishspace(*p) ; ++p, --len) ;\r
480 \r
481     /* keep going until we run out of string */\r
482     for (;;)\r
483     {\r
484         const char *start;\r
485         \r
486         /* scan to the next whitespace or markup-significant character */\r
487         for (start = p ;\r
488              len != 0 && !u_ishspace(*p)\r
489              && *p != '<' && *p != '>' && *p != '&' ; ++p, --len) ;\r
490 \r
491         /* write the part up to here */\r
492         if (p != start)\r
493             write_ifiction(ctx, start, p - start);\r
494 \r
495         /* if we've reached the end of the string, we can stop */\r
496         if (len == 0)\r
497             break;\r
498 \r
499         /* check what stopped us */\r
500         switch (*p)\r
501         {\r
502         case '<':\r
503             write_ifiction_z(ctx, "&lt;");\r
504             ++p, --len;\r
505             break;\r
506 \r
507         case '>':\r
508             write_ifiction_z(ctx, "&gt;");\r
509             ++p, --len;\r
510             break;\r
511 \r
512         case '&':\r
513             write_ifiction_z(ctx, "&amp;");\r
514             ++p, --len;\r
515             break;\r
516 \r
517         default:\r
518             /* \r
519              *   The only other thing that could have stopped us is\r
520              *   whitespace.  Skip all consecutive whitespace. \r
521              */\r
522             for ( ; len != 0 && u_ishspace(*p) ; ++p, --len);\r
523 \r
524             /* \r
525              *   if that's not the end of the string, replace the run of\r
526              *   whitespace with a single space character in the output; if\r
527              *   we've reached the end of the string, we don't even want to\r
528              *   do that, since we want to trim off trailing spaces \r
529              */\r
530             if (len != 0)\r
531                 write_ifiction_z(ctx, " ");\r
532             break;\r
533         }\r
534     }\r
535 }\r
536 \r
537 /*\r
538  *   Translate a GameInfo keyed value to the corresponding ifiction tagged\r
539  *   value.  We find the GameInfo value keyed by 'gameinfo_key', and write\r
540  *   out the same string under the ifiction XML tag 'ifiction_tag'.  We write\r
541  *   a complete XML container sequence - <tag>value</tag>.\r
542  *   \r
543  *   If the given GameInfo key doesn't exist, we use the default value string\r
544  *   'dflt', if given.  If the GameInfo key doesn't exist and 'dflt' is null,\r
545  *   we don't write anything - we don't even write the open/close tags.\r
546  *   \r
547  *   If 'html' is true, we assume the value is in html format, and we write\r
548  *   it untranslated.  Otherwise, we write it as PCDATA, translating markup\r
549  *   characters into '&' entities and compressing whitespace.  \r
550  */\r
551 static void write_ifiction_xlat_base(synthctx *ctx, int indent,\r
552                                      const char *gameinfo_key,\r
553                                      const char *ifiction_tag,\r
554                                      const char *dflt, int html)\r
555 {\r
556     valinfo *val;\r
557     const char *valstr;\r
558     size_t vallen;\r
559     \r
560     /* look up the GameInfo key */\r
561     if ((val = find_by_key(ctx->vals, gameinfo_key)) != 0)\r
562     {\r
563         /* we found the GameInfo value - use it */\r
564         valstr = val->val;\r
565         vallen = val->val_len;\r
566     }\r
567     else if (dflt != 0)\r
568     {\r
569         /* the GameInfo value doesn't exist, but we have a default - use it */\r
570         valstr = dflt;\r
571         vallen = strlen(dflt);\r
572     }\r
573     else\r
574     {\r
575         /* there's no GameInfo value and no default, so write nothing */\r
576         return;\r
577     }\r
578 \r
579     /* write the indentation */\r
580     while (indent != 0)\r
581     {\r
582         static const char spaces[] = "          ";\r
583         size_t cur;\r
584 \r
585         /* figure how much we can write on this round */\r
586         cur = indent;\r
587         if (cur > sizeof(spaces) - 1)\r
588             cur = sizeof(spaces) - 1;\r
589 \r
590         /* write it */\r
591         write_ifiction(ctx, spaces, cur);\r
592 \r
593         /* deduct it from the amount remaining */\r
594         indent -= cur;\r
595     }\r
596 \r
597     /* write the open tag */\r
598     write_ifiction_z(ctx, "<");\r
599     write_ifiction_z(ctx, ifiction_tag);\r
600     write_ifiction_z(ctx, ">");\r
601 \r
602     /* write the value, applying pcdata translations */\r
603     if (html)\r
604         write_ifiction(ctx, valstr, vallen);\r
605     else\r
606         write_ifiction_pcdata(ctx, valstr, vallen);\r
607 \r
608     /* write the close tag */\r
609     write_ifiction_z(ctx, "</");\r
610     write_ifiction_z(ctx, ifiction_tag);\r
611     write_ifiction_z(ctx, ">\n");\r
612 }\r
613 \r
614 #define write_ifiction_xlat(ctx, indent, gikey, iftag, dflt) \\r
615     write_ifiction_xlat_base(ctx, indent, gikey, iftag, dflt, FALSE)\r
616 \r
617 #define write_ifiction_xlat_html(ctx, indent, gikey, iftag, dflt) \\r
618     write_ifiction_xlat_base(ctx, indent, gikey, iftag, dflt, TRUE)\r
619 \r
620 \r
621 /*\r
622  *   Retrieve the next author name from the GameInfo "Author" format.  The\r
623  *   format is as follows:\r
624  *   \r
625  *   name <email> <email>... ; ...\r
626  *   \r
627  *   That is, each author is listed with a name followed by one or more email\r
628  *   addresses in angle brackets, and multiple authors are separated by\r
629  *   semicolons.  \r
630  */\r
631 static int scan_author_name(const char **p, size_t *len,\r
632                             const char **start, const char **end)\r
633 {\r
634     /* keep going until we find a non-empty author name */\r
635     for (;;)\r
636     {\r
637         /* skip leading spaces */\r
638         for ( ; *len != 0 && u_ishspace(**p) ; ++*p, --*len) ;\r
639 \r
640         /* if we ran out of string, there's definitely no author name */\r
641         if (*len == 0)\r
642             return FALSE;\r
643 \r
644         /* \r
645          *   Find the end of this author name.  The author name ends at the\r
646          *   next semicolon or angle bracket.  \r
647          */\r
648         for (*start = *p ; *len != 0 && **p != ';' && **p != '<' ;\r
649              ++*p, --*len) ;\r
650 \r
651         /* trim off any trailing spaces */\r
652         for (*end = *p ; *end > *start && u_ishspace(*(*end - 1)) ; --*end) ;\r
653 \r
654         /* now skip any email addresses */\r
655         while (*len != 0 && **p == '<')\r
656         {\r
657             /* skip to the closing bracket */\r
658             for (++*p, --*len ; *len != 0 && **p != '>' ; ++*p, --*len) ;\r
659 \r
660             /* skip the bracket */\r
661             if (*len != 0)\r
662                 ++*p, --*len;\r
663 \r
664             /* skip whitespace */\r
665             for ( ; *len != 0 && u_ishspace(**p) ; ++*p, --*len) ;\r
666 \r
667             /* \r
668              *   if we're not at a semicolon, another angle bracket, or the\r
669              *   end of the string, it's a syntax error \r
670              */\r
671             if (*len != 0 && **p != '<' && **p != ';')\r
672             {\r
673                 *len = 0;\r
674                 return FALSE;\r
675             }\r
676         }\r
677 \r
678         /* if we're at a semicolon, skip it */\r
679         if (*len != 0 && **p == ';')\r
680             ++*p, --*len;\r
681 \r
682         /* \r
683          *   if we found a non-empty name, return it; otherwise, continue on\r
684          *   to the next semicolon section \r
685          */\r
686         if (*end != *start)\r
687             return TRUE;\r
688     }\r
689 }\r
690 \r
691 \r
692 /*\r
693  *   Synthesize an ifiction record for the given GameInfo name/value pair\r
694  *   list.  Returns the number of bytes required for the result, including\r
695  *   null termination.  We'll copy as much as we can to the output buffer, up\r
696  *   to bufsize; if the buffer size is insufficient to hold the result, we'll\r
697  *   still indicate the length needed for the full result, but we're careful\r
698  *   not to actually copy anything past the end of the buffer.  \r
699  */\r
700 static int32 synth_ifiction(valinfo *vals, int tads_version,\r
701                             char *buf, int32 bufsize,\r
702                             void *story_file, int32 extent)\r
703 {\r
704     char default_ifid[TREATY_MINIMUM_EXTENT];\r
705     valinfo *ifid = find_by_key(vals, "IFID");\r
706     const char *ifid_val;\r
707     size_t ifid_len;\r
708     valinfo *author = find_by_key(vals, "AuthorEmail");\r
709     valinfo *url = find_by_key(vals, "Url");\r
710     synthctx ctx;\r
711     const char *p;\r
712     size_t rem;\r
713     int32 art_fmt;\r
714     int32 art_wid, art_ht;\r
715 \r
716     /* initialize the output content */\r
717     init_synthctx(&ctx, buf, bufsize, vals);\r
718 \r
719     /* make sure the tads version is one we know how to handle */\r
720     if (tads_version != 2 && tads_version != 3)\r
721         return NO_REPLY_RV;\r
722 \r
723     /* \r
724      *   The IFID is mandatory.  If there's not an IFID specifically listed\r
725      *   in the GameInfo, we need to generate the default IFID based on the\r
726      *   MD5 hash of the game file. \r
727      */\r
728     if (ifid != 0)\r
729     {\r
730         /* use the explicit IFID(s) listed in the GameInfo */\r
731         ifid_val = ifid->val;\r
732         ifid_len = ifid->val_len;\r
733     }\r
734     else\r
735     {\r
736         /* generate the default IFID */\r
737         generate_md5_ifid(story_file, extent,\r
738                           default_ifid, TREATY_MINIMUM_EXTENT);\r
739 \r
740         /* use this as the IFID */\r
741         ifid_val = default_ifid;\r
742         ifid_len = strlen(default_ifid);\r
743     }\r
744 \r
745     /* write the header, and start the <identification> section */\r
746     write_ifiction_z(\r
747         &ctx,\r
748         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"\r
749         "<ifindex version=\"1.0\" "\r
750         "xmlns=\"http://babel.ifarchive.org/protocol/iFiction/\">\n"\r
751         "  <!-- Bibliographic data translated from TADS GameInfo -->\n"\r
752         "  <story>\n"\r
753         "    <colophon>\n"\r
754         "     <generator>Babel</generator>\n"\r
755         "     <generatorversion>" TREATY_VERSION "</generatorversion>\n"\r
756         "      <originated>2006-04-14</originated>\n"\r
757         "     </colophon>\n"\r
758         "    <identification>\n");\r
759 \r
760     /* write each IFID (there might be several) */\r
761     for (p = ifid_val, rem = ifid_len ; rem != 0 ; )\r
762     {\r
763         const char *start;\r
764         const char *end;\r
765 \r
766         /* skip leading spaces */\r
767         for ( ; rem != 0 && u_ishspace(*p) ; ++p, --rem) ;\r
768         \r
769         /* find the end of this IFID */\r
770         for (start = p ; rem != 0 && *p != ',' ; ++p, --rem) ;\r
771 \r
772         /* remove trailing spaces */\r
773         for (end = p ; end > start && u_ishspace(*(end-1)) ; --end) ;\r
774 \r
775         /* if we found one, write it out */\r
776         if (end != start)\r
777         {\r
778             write_ifiction_z(&ctx, "      <ifid>");\r
779             write_ifiction(&ctx, start, end - start);\r
780             write_ifiction_z(&ctx, "</ifid>\n");\r
781         }\r
782 \r
783         /* skip the comma */\r
784         if (rem != 0 && *p == ',')\r
785             ++p, --rem;\r
786     }\r
787 \r
788     /* add the format information */\r
789     write_ifiction_z(&ctx,\r
790                      tads_version == 2\r
791                      ? "      <format>tads2</format>\n"\r
792                      : "      <format>tads3</format>\n");\r
793 \r
794     /* close the <identification> section and start the <bibliographic> */\r
795     write_ifiction_z(&ctx,\r
796                      "    </identification>\n"\r
797                      "    <bibliographic>\n");\r
798 \r
799     /* write the various bibliographic data */\r
800     write_ifiction_xlat(&ctx, 6, "Name", "title", "An Interactive Fiction");\r
801     write_ifiction_xlat(&ctx, 6, "Headline", "headline", 0);\r
802     write_ifiction_xlat(&ctx, 6, "Desc", "description", 0);\r
803     write_ifiction_xlat(&ctx, 6, "Genre", "genre", 0);\r
804     write_ifiction_xlat(&ctx, 6, "Forgiveness", "forgiveness", 0);\r
805     write_ifiction_xlat(&ctx, 6, "Series", "series", 0);\r
806     write_ifiction_xlat(&ctx, 6, "SeriesNumber", "seriesnumber", 0);\r
807     write_ifiction_xlat(&ctx, 6, "Language", "language", 0);\r
808     write_ifiction_xlat(&ctx, 6, "FirstPublished", "firstpublished", 0);\r
809 \r
810     /* if there's an author, write the list of author names */\r
811     if (author != 0)\r
812     {\r
813         int cnt;\r
814         int i;\r
815         const char *start;\r
816         const char *end;\r
817 \r
818         /* start the <author> tag */\r
819         write_ifiction_z(&ctx, "      <author>");\r
820         \r
821         /* \r
822          *   first, count up the number of authors - authors are separated by\r
823          *   semicolons, so there's one more author than there are semicolons\r
824          */\r
825         for (p = author->val, rem = author->val_len, cnt = 1 ;\r
826              scan_author_name(&p, &rem, &start, &end) ; ) ;\r
827 \r
828         /* \r
829          *   Now generate the list of authors.  If there are multiple\r
830          *   authors, use commas to separate them. \r
831          */\r
832         for (p = author->val, rem = author->val_len, i = 0 ; ; ++i)\r
833         {\r
834             /* scan this author's name */\r
835             if (!scan_author_name(&p, &rem, &start, &end))\r
836                 break;\r
837             \r
838             /* write out this author name */\r
839             write_ifiction_pcdata(&ctx, start, end - start);\r
840 \r
841             /* if there's another name to come, write a separator */\r
842             if (i + 1 < cnt)\r
843             {\r
844                 /* \r
845                  *   write just "and" to separate two items; write ","\r
846                  *   between items in lists of more than two, with ",and"\r
847                  *   between the last two items \r
848                  */\r
849                 write_ifiction_z(&ctx,\r
850                                  cnt == 2 ? " and " :\r
851                                  i + 2 < cnt ? ", " : ", and ");\r
852             }\r
853         }\r
854 \r
855         /* end the <author> tag */\r
856         write_ifiction_z(&ctx, "</author>\n");\r
857     }\r
858 \r
859     /* end the biblio section */\r
860     write_ifiction_z(&ctx, "    </bibliographic>\n");\r
861 \r
862     /* if there's cover art, add its information */\r
863     if (find_cover_art(story_file, extent, 0, &art_fmt, &art_wid, &art_ht)\r
864         && (art_fmt == PNG_COVER_FORMAT || art_fmt == JPEG_COVER_FORMAT))\r
865     {\r
866         char buf[200];\r
867         \r
868         sprintf(buf,\r
869                 "    <cover>\n"\r
870                 "        <format>%s</format>\n"\r
871                 "        <height>%lu</height>\n"\r
872                 "        <width>%lu</width>\n"\r
873                 "    </cover>\n",\r
874                 art_fmt == PNG_COVER_FORMAT ? "png" : "jpg",\r
875                 (long)art_ht, (long)art_wid);\r
876 \r
877         write_ifiction_z(&ctx, buf);\r
878     }\r
879 \r
880     /* if there's an author email, include it */\r
881     if (author != 0 || url != 0)\r
882     {\r
883         const char *p;\r
884         size_t rem;\r
885         int i;\r
886         \r
887         /* open the section */\r
888         write_ifiction_z(&ctx, "    <contacts>\n");\r
889 \r
890         /* add the author email, if provided */\r
891         if (author != 0)\r
892         {\r
893             /* write the email list */\r
894             for (i = 0, p = author->val, rem = author->val_len ; ; ++i)\r
895             {\r
896                 const char *start;\r
897                 \r
898                 /* skip to the next email address */\r
899                 for ( ; rem != 0 && *p != '<' ; ++p, --rem) ;\r
900                 \r
901                 /* if we didn't find an email address, we're done */\r
902                 if (rem == 0)\r
903                     break;\r
904                 \r
905                 /* find the matching '>' */\r
906                 for (++p, --rem, start = p ; rem != 0 && *p != '>' ;\r
907                      ++p, --rem) ;\r
908 \r
909                 /* \r
910                  *   if this is the first one, open the section; otherwise,\r
911                  *   add a comma \r
912                  */\r
913                 if (i == 0)\r
914                     write_ifiction_z(&ctx, "      <authoremail>");\r
915                 else\r
916                     write_ifiction_z(&ctx, ",");\r
917                 \r
918                 /* write this address */\r
919                 write_ifiction(&ctx, start, p - start);\r
920                 \r
921                 /* \r
922                  *   skip the closing bracket, if there is one; if we're out\r
923                  *   of string, we're done \r
924                  */\r
925                 if (rem != 0)\r
926                     ++p, --rem;\r
927                 else\r
928                     break;\r
929             }\r
930 \r
931             /* if we found any emails to write, end the section */\r
932             if (i != 0)\r
933                 write_ifiction_z(&ctx, "</authoremail>\n");\r
934         }\r
935 \r
936         /* if there's a URL, add it */\r
937         if (url != 0)\r
938         {\r
939             write_ifiction_z(&ctx, "      <url>");\r
940             write_ifiction(&ctx, url->val, url->val_len);\r
941             write_ifiction_z(&ctx, "</url>\n");\r
942         }\r
943 \r
944         /* close the section */\r
945         write_ifiction_z(&ctx, "    </contacts>\n");\r
946     }\r
947 \r
948     /* add the tads-specific section */\r
949     write_ifiction_z(&ctx, "    <tads>\n");\r
950     \r
951     write_ifiction_xlat(&ctx, 6, "Version", "version", 0);\r
952     write_ifiction_xlat(&ctx, 6, "ReleaseDate", "releasedate", 0);\r
953     write_ifiction_xlat(&ctx, 6, "PresentationProfile",\r
954                         "presentationprofile", 0);\r
955     write_ifiction_xlat(&ctx, 6, "Byline", "byline", 0);\r
956 \r
957     write_ifiction_z(&ctx, "    </tads>\n");\r
958 \r
959     /* close the story section and the main body */\r
960     write_ifiction_z(&ctx, "  </story>\n</ifindex>\n");\r
961     \r
962     /* add the null terminator */\r
963     write_ifiction(&ctx, "", 1);\r
964 \r
965     /* return the total output size */\r
966     return ctx.total_size;\r
967 }\r
968 \r
969 /* ------------------------------------------------------------------------ */\r
970 /*\r
971  *   Check a data block to see if it starts with the given signature. \r
972  */\r
973 int tads_match_sig(const void *buf, int32 len, const char *sig)\r
974 {\r
975     /* note the length of the signature string */\r
976     size_t sig_len = strlen(sig);\r
977     \r
978     /* if matches if the buffer starts with the signature string */\r
979     return (len >= (int32)sig_len && memcmp(buf, sig, sig_len) == 0);\r
980 }\r
981 \r
982 \r
983 /* ------------------------------------------------------------------------ */\r
984 /*\r
985  *   portable-to-native format conversions \r
986  */\r
987 #define osbyte(p, ofs) \\r
988     (*(((unsigned char *)(p)) + (ofs)))\r
989 \r
990 #define osrp1(p) \\r
991     ((unsigned int)osbyte(p, 0))\r
992 \r
993 #define osrp2(p) \\r
994     ((unsigned int)osbyte(p, 0) \\r
995     + ((unsigned int)osbyte(p, 1) << 8))\r
996 \r
997 #define osrp4(p) \\r
998     (((unsigned long)osbyte(p, 0)) \\r
999     + (((unsigned long)osbyte(p, 1)) << 8) \\r
1000     + (((unsigned long)osbyte(p, 2)) << 16) \\r
1001     + (((unsigned long)osbyte(p, 3)) << 24))\r
1002 \r
1003 \r
1004 /* ------------------------------------------------------------------------ */\r
1005 /*\r
1006  *   Parse a game file and retrieve the GameInfo data.  Returns the head of a\r
1007  *   linked list of valinfo entries.\r
1008  */\r
1009 static valinfo *parse_game_info(const void *story_file, int32 story_len,\r
1010                                 int *tads_version)\r
1011 {\r
1012     resinfo res;\r
1013     const char *p;\r
1014     int32 rem;\r
1015     valinfo *val_head = 0;\r
1016 \r
1017     /* \r
1018      *   first, find the GameInfo resource - if it's not there, there's no\r
1019      *   game information to parse \r
1020      */\r
1021     if (!find_resource(story_file, story_len, "GameInfo.txt", &res))\r
1022         return 0;\r
1023 \r
1024     /* if the caller wants the TADS version number, hand it back */\r
1025     if (tads_version != 0)\r
1026         *tads_version = res.tads_version;\r
1027 \r
1028     /* parse the data */\r
1029     for (p = res.ptr, rem = res.len ; rem != 0 ; )\r
1030     {\r
1031         const char *name_start;\r
1032         size_t name_len;\r
1033         const char *val_start;\r
1034         valinfo *val;\r
1035         const char *inp;\r
1036         int32 inlen;\r
1037         char *outp;\r
1038 \r
1039         /* skip any leading whitespace */\r
1040         while (rem != 0 && u_isspace(*p))\r
1041             ++p, --rem;\r
1042 \r
1043         /* if the line starts with '#', it's a comment, so skip it */\r
1044         if (rem != 0 && *p == '#')\r
1045         {\r
1046             skip_to_next_line(&p, &rem);\r
1047             continue;\r
1048         }\r
1049 \r
1050         /* we must have the start of a name - note it */\r
1051         name_start = p;\r
1052 \r
1053         /* skip ahead to a space or colon */\r
1054         while (rem != 0 && *p != ':' && !u_ishspace(*p))\r
1055             nextc(&p, &rem);\r
1056 \r
1057         /* note the length of the name */\r
1058         name_len = p - name_start;\r
1059 \r
1060         /* skip any whitespace before the presumed colon */\r
1061         while (rem != 0 && u_ishspace(*p))\r
1062             nextc(&p, &rem);\r
1063 \r
1064         /* if we're not at a colon, the line is ill-formed, so skip it */\r
1065         if (rem == 0 || *p != ':')\r
1066         {\r
1067             /* skip the entire line, and go back for the next one */\r
1068             skip_to_next_line(&p, &rem);\r
1069             continue;\r
1070         }\r
1071 \r
1072         /* skip the colon and any whitespace immediately after it */\r
1073         for (nextc(&p, &rem) ; rem != 0 && u_ishspace(*p) ; nextc(&p, &rem)) ;\r
1074 \r
1075         /* note where the value starts */\r
1076         val_start = p;\r
1077 \r
1078         /*\r
1079          *   Scan the value to get its length.  The value runs from here to\r
1080          *   the next newline that's not followed immediately by a space. \r
1081          */\r
1082         while (rem != 0)\r
1083         {\r
1084             const char *nl;\r
1085             int32 nlrem;\r
1086             \r
1087             /* skip to the next line */\r
1088             skip_to_next_line(&p, &rem);\r
1089 \r
1090             /* if we're at eof, we can stop now */\r
1091             if (rem == 0)\r
1092                 break;\r
1093 \r
1094             /* note where this line starts */\r
1095             nl = p;\r
1096             nlrem = rem;\r
1097 \r
1098             /* \r
1099              *   if we're at a non-whitespace character, it's definitely not\r
1100              *   a continuation line \r
1101              */\r
1102             if (!u_ishspace(*p))\r
1103                 break;\r
1104 \r
1105             /* \r
1106              *   check for spaces followed by a non-space character - this\r
1107              *   would signify a continuation line\r
1108              */\r
1109             for ( ; rem != 0 && u_ishspace(*p) ; nextc(&p, &rem)) ;\r
1110             if (rem == 0 || u_isnl(p, rem))\r
1111             {\r
1112                 /* \r
1113                  *   we're at end of file, we found a line with nothing but\r
1114                  *   whitespace, so this isn't a continuation line; go back\r
1115                  *   to the start of this line and end the value here \r
1116                  */\r
1117                 p = nl;\r
1118                 rem = nlrem;\r
1119                 break;\r
1120             }\r
1121 \r
1122             /* \r
1123              *   We found whitespace followed by non-whitespace, so this is a\r
1124              *   continuation line.  Keep going for now.\r
1125              */\r
1126         }\r
1127 \r
1128         /* remove any trailing newlines */\r
1129         while (p > val_start)\r
1130         {\r
1131             /* move back one character */\r
1132             prevc(&p, &rem);\r
1133 \r
1134             /* \r
1135              *   if it's a newline, keep going; otherwise, keep this\r
1136              *   character and stop trimming \r
1137              */\r
1138             if (!u_isnl(p, rem))\r
1139             {\r
1140                 nextc(&p, &rem);\r
1141                 break;\r
1142             }\r
1143         }\r
1144 \r
1145         /* \r
1146          *   Allocate a new value entry.  Make room for the entry itself plus\r
1147          *   a copy of the value.  We don't need to make a copy of the name,\r
1148          *   since we can just use the original copy from the story file\r
1149          *   buffer.  We do need a copy of the value because we might need to\r
1150          *   transform it slightly, to remove newlines and leading spaces on\r
1151          *   continuation lines. \r
1152          */\r
1153         val = (valinfo *)malloc(sizeof(valinfo) + (p - val_start));\r
1154 \r
1155         /* link it into our list */\r
1156         val->nxt = val_head;\r
1157         val_head = val;\r
1158 \r
1159         /* point the name directly to the name in the buffer */\r
1160         val->name = name_start;\r
1161         val->name_len = name_len;\r
1162 \r
1163         /* point the value to the space allocated along with the valinfo */\r
1164         val->val = (char *)(val + 1);\r
1165 \r
1166         /* store the name, removing newlines and continuation-line spaces */\r
1167         for (outp = val->val, inp = val_start, inlen = p - val_start ;\r
1168              inlen != 0 ; )\r
1169         {\r
1170             const char *l;\r
1171             \r
1172             /* find the next newline */\r
1173             for (l = inp ; inlen != 0 && !u_isnl(inp, inlen) ;\r
1174                  nextc(&inp, &inlen)) ;\r
1175 \r
1176             /* copy this line to the output */\r
1177             memcpy(outp, l, inp - l);\r
1178             outp += inp - l;\r
1179 \r
1180             /* if we're out of input, we're done */\r
1181             if (inlen == 0)\r
1182                 break;\r
1183 \r
1184             /* we're at a newline: replace it with a space in the output */\r
1185             *outp++ = ' ';\r
1186 \r
1187             /* skip the newline and subsequent whitespace in the input */\r
1188             for (skip_newline(&inp, &inlen) ;\r
1189                  inlen != 0 && u_ishspace(*inp) ; nextc(&inp, &inlen)) ;\r
1190         }\r
1191 \r
1192         /* set the length of the parsed value */\r
1193         val->val_len = outp - val->val;\r
1194 \r
1195         /* skip to the next line and continue parsing */\r
1196         skip_to_next_line(&p, &rem);\r
1197     }\r
1198 \r
1199     /* return the head of the linked list of value entries */\r
1200     return val_head;\r
1201 }\r
1202 static int my_memicmp(const void *aa, const void *bb, int l)\r
1203 {\r
1204  int s=0,i;\r
1205  char *a=(char *) aa;\r
1206  char *b=(char *) bb;\r
1207  for(i=0;i<l && !s;i++)\r
1208   s=tolower(a[i])-tolower(b[i]);\r
1209  return s;\r
1210 }\r
1211 \r
1212 /* ------------------------------------------------------------------------ */\r
1213 /*\r
1214  *   Given a valinfo list obtained from parse_game_info(), find the value for\r
1215  *   the given key \r
1216  */\r
1217 static valinfo *find_by_key(valinfo *list_head, const char *key)\r
1218 {\r
1219     valinfo *p;\r
1220     size_t key_len = strlen(key);\r
1221     \r
1222     /* scan the list for the given key */\r
1223     for (p = list_head ; p != 0 ; p = p->nxt)\r
1224     {\r
1225         /* if this one matches the key we're looking for, return it */\r
1226         if (p->name_len == key_len && my_memicmp(p->name, key, key_len) == 0)\r
1227             return p;\r
1228     }\r
1229 \r
1230     /* no luck */\r
1231     return 0;\r
1232 }\r
1233 \r
1234 /* ------------------------------------------------------------------------ */\r
1235 /*\r
1236  *   Delete a valinfo list obtained from parse_game_info() \r
1237  */\r
1238 static void delete_valinfo_list(valinfo *head)\r
1239 {\r
1240     /* keep going until we run out of entries */\r
1241     while (head != 0)\r
1242     {\r
1243         /* remember the next entry, before we delete this one */\r
1244         valinfo *nxt = head->nxt;\r
1245 \r
1246         /* delete this one */\r
1247         free(head);\r
1248 \r
1249         /* move on to the next one */\r
1250         head = nxt;\r
1251     }\r
1252 }\r
1253 \r
1254 /* ------------------------------------------------------------------------ */\r
1255 /*\r
1256  *   Find the cover art resource.  We'll look for CoverArt.jpg and\r
1257  *   CoverArt.png, in that order. \r
1258  */\r
1259 static int find_cover_art(const void *story_file, int32 story_len,\r
1260                           resinfo *resp, int32 *image_format,\r
1261                           int32 *width, int32 *height)\r
1262 {\r
1263     resinfo res;\r
1264     int32 x, y;\r
1265 \r
1266     /* if they didn't want the resource info, provide a placeholder */\r
1267     if (resp == 0)\r
1268         resp = &res;\r
1269 \r
1270     /* look for CoverArt.jpg first */\r
1271     if (find_resource(story_file, story_len, "CoverArt.jpg", resp))\r
1272     {\r
1273         /* get the width and height */\r
1274         if (!get_jpeg_dim(resp->ptr, resp->len, &x, &y))\r
1275             return FALSE;\r
1276 \r
1277         /* hand back the width and height if it was requested */\r
1278         if (width != 0)\r
1279             *width = x;\r
1280         if (height != 0)\r
1281             *height = y;\r
1282 \r
1283         /* tell them it's a JPEG image */\r
1284         if (image_format != 0)\r
1285             *image_format = JPEG_COVER_FORMAT;\r
1286 \r
1287         /* indicate success */\r
1288         return TRUE;\r
1289     }\r
1290 \r
1291     /* look for CoverArt.png second */\r
1292     if (find_resource(story_file, story_len, "CoverArt.png", resp))\r
1293     {\r
1294         /* get the width and height */\r
1295         if (!get_png_dim(resp->ptr, resp->len, &x, &y))\r
1296             return FALSE;\r
1297 \r
1298         /* hand back the width and height if it was requested */\r
1299         if (width != 0)\r
1300             *width = x;\r
1301         if (height != 0)\r
1302             *height = y;\r
1303 \r
1304         /* tell them it's a PNG image */\r
1305         if (image_format != 0)\r
1306             *image_format = PNG_COVER_FORMAT;\r
1307 \r
1308         /* indicate success */\r
1309         return TRUE;\r
1310     }\r
1311 \r
1312     /* didn't find it */\r
1313     return FALSE;\r
1314 }\r
1315 \r
1316 /* ------------------------------------------------------------------------ */\r
1317 /*\r
1318  *   Find a resource in a TADS 2 or 3 story file that's been loaded into\r
1319  *   memory.  On success, fills in the offset and size of the resource and\r
1320  *   returns TRUE; if the resource isn't found, returns FALSE.\r
1321  */\r
1322 static int find_resource(const void *story_file, int32 story_len,\r
1323                          const char *resname, resinfo *info)\r
1324 {\r
1325     /* if there's no file, there's no resource */\r
1326     if (story_file == 0)\r
1327         return FALSE;\r
1328 \r
1329     /* check for tads 2 */\r
1330     if (tads_match_sig(story_file, story_len, T2_SIGNATURE))\r
1331     {\r
1332         info->tads_version = 2;\r
1333         return t2_find_res(story_file, story_len, resname, info);\r
1334     }\r
1335 \r
1336     /* check for tads 3 */\r
1337     if (tads_match_sig(story_file, story_len, T3_SIGNATURE))\r
1338     {\r
1339         info->tads_version = 3;\r
1340         return t3_find_res(story_file, story_len, resname, info);\r
1341     }\r
1342 \r
1343     /* it's not one of ours */\r
1344     return FALSE;\r
1345 }\r
1346 \r
1347 /* ------------------------------------------------------------------------ */\r
1348 /*\r
1349  *   Find a resource in a tads 2 game file \r
1350  */\r
1351 static int t2_find_res(const void *story_file, int32 story_len,\r
1352                        const char *resname, resinfo *info)\r
1353 {\r
1354     const char *basep = (const char *)story_file;\r
1355     const char *endp = basep + story_len;\r
1356     const char *p;\r
1357     size_t resname_len;\r
1358 \r
1359     /* note the length of the name we're seeking */\r
1360     resname_len = strlen(resname);\r
1361 \r
1362     /* \r
1363      *   skip past the tads 2 file header (13 bytes for the signature, 7\r
1364      *   bytes for the version header, 2 bytes for the flags, 26 bytes for\r
1365      *   the timestamp) \r
1366      */\r
1367     p = basep + 13 + 7 + 2 + 26;\r
1368 \r
1369     /* \r
1370      *   scan the sections in the file; stop on $EOF, and skip everything\r
1371      *   else but HTMLRES, which is the section type that \r
1372      */\r
1373     while (p < endp)\r
1374     {\r
1375         unsigned long endofs;\r
1376 \r
1377         /*\r
1378          *   We're pointing to a section block header, which looks like this:\r
1379          *   \r
1380          *.    <byte> type-length\r
1381          *.    <byte * type-length> type-name\r
1382          *.    <uint32> next-section-address\r
1383          */\r
1384 \r
1385         /* read the ending offset */\r
1386         endofs = osrp4(p + 1 + osrp1(p));\r
1387 \r
1388         /* check the type */\r
1389         if (p[0] == 7 && memcmp(p + 1, "HTMLRES", 7) == 0)\r
1390         {\r
1391             unsigned long found_ofs;\r
1392             int found;\r
1393             unsigned long entry_cnt;\r
1394 \r
1395             /* we haven't found the resource yet */\r
1396             found = FALSE;\r
1397 \r
1398             /* \r
1399              *   It's a multimedia resource block.  Skip the section block\r
1400              *   header and look at the index table - the index table\r
1401              *   consists of a uint32 giving the number of entries, followed\r
1402              *   by a reserved uint32, followed by the entries.  \r
1403              */\r
1404             p += 12;\r
1405             entry_cnt = osrp4(p);\r
1406 \r
1407             /* skip to the first index entry */\r
1408             p += 8;\r
1409 \r
1410             /* scan the index entries */\r
1411             for ( ; entry_cnt != 0 ; --entry_cnt)\r
1412             {\r
1413                 unsigned long res_ofs;\r
1414                 unsigned long res_siz;\r
1415                 size_t name_len;\r
1416 \r
1417                 /*\r
1418                  *   We're at the next index entry, which looks like this:\r
1419                  *\r
1420                  *.    <uint32>  resource-address (bytes from end of index)\r
1421                  *.    <uint32>  resource-length (in bytes)\r
1422                  *.    <uint2> name-length\r
1423                  *.    <byte * name-length> name\r
1424                  */\r
1425                 res_ofs = osrp4(p);\r
1426                 res_siz = osrp4(p + 4);\r
1427                 name_len = osrp2(p + 8);\r
1428                 p += 10;\r
1429 \r
1430                 /* check for a match to the name we're looking for */\r
1431                 if (name_len == resname_len\r
1432                     && my_memicmp(resname, p, name_len) == 0)\r
1433                 {\r
1434                     /* \r
1435                      *   it's the one we want - note its resource location\r
1436                      *   and size, but keep scanning for now, since we need\r
1437                      *   to find the end of the index before we'll know where\r
1438                      *   the actual resources begin \r
1439                      */\r
1440                     found = TRUE;\r
1441                     found_ofs = res_ofs;\r
1442                     info->len = res_siz;\r
1443                 }\r
1444 \r
1445                 /* skip this one's name */\r
1446                 p += name_len;\r
1447             }\r
1448 \r
1449             /* \r
1450              *   if we found our resource, the current seek position is the\r
1451              *   base of the offset we found in the directory; so we can\r
1452              *   finally fix up the offset to give the actual file location\r
1453              *   and return the result \r
1454              */\r
1455             if (found)\r
1456             {\r
1457                 /* fix up the offset with the actual file location */\r
1458                 info->ptr = p + found_ofs;\r
1459 \r
1460                 /* tell the caller we found it */\r
1461                 return TRUE;\r
1462             }\r
1463         }\r
1464         else if (p[0] == 4 && memcmp(p + 1, "$EOF", 4) == 0)\r
1465         {\r
1466             /* \r
1467              *   that's the end of the file - we've finished without finding\r
1468              *   the resource, so return failure \r
1469              */\r
1470             return FALSE;\r
1471         }\r
1472 \r
1473         /* move to the next section */\r
1474         p = basep + endofs;\r
1475     }\r
1476 \r
1477     /* \r
1478      *   reached EOF without an $EOF marker - file must be corrupted; return\r
1479      *   'not found' \r
1480      */\r
1481     return FALSE;\r
1482 }\r
1483 \r
1484 /* ------------------------------------------------------------------------ */\r
1485 /*\r
1486  *   Find a resource in a T3 image file \r
1487  */\r
1488 static int t3_find_res(const void *story_file, int32 story_len,\r
1489                        const char *resname, resinfo *info)\r
1490 {\r
1491     const char *basep = (const char *)story_file;\r
1492     const char *endp = basep + story_len;\r
1493     const char *p;\r
1494     size_t resname_len;\r
1495 \r
1496     /* note the length of the name we're seeking */\r
1497     resname_len = strlen(resname);\r
1498 \r
1499     /* \r
1500      *   skip the file header - 11 bytes for the signature, 2 bytes for the\r
1501      *   format version, 32 reserved bytes, and 24 bytes for the timestamp \r
1502      */\r
1503     p = basep + 11 + 2 + 32 + 24;\r
1504 \r
1505     /* scan the data blocks */\r
1506     while (p < endp)\r
1507     {\r
1508         unsigned long siz;\r
1509 \r
1510         /*\r
1511          *   We're at the next block header, which looks like this:\r
1512          *\r
1513          *.    <byte * 4> type-name\r
1514          *.    <uint32> block-size\r
1515          *.    <uint16> flags\r
1516          */\r
1517 \r
1518         /* get the block size */\r
1519         siz = osrp4(p + 4);\r
1520 \r
1521         /* check the type */\r
1522         if (memcmp(p, "MRES", 4) == 0)\r
1523         {\r
1524             unsigned int entry_cnt;\r
1525             unsigned int i;\r
1526             const char *blockp;\r
1527 \r
1528             /* skip the header */\r
1529             p += 10;\r
1530 \r
1531             /* \r
1532              *   remember the location of the base of the block - the data\r
1533              *   seek location for each index entry is given as an offset\r
1534              *   from this location \r
1535              */\r
1536             blockp = p;\r
1537 \r
1538             /* the first thing in the table is the number of entries */\r
1539             entry_cnt = osrp2(p);\r
1540             p += 2;\r
1541 \r
1542             /* read the entries */\r
1543             for (i = 0 ; i < entry_cnt ; ++i)\r
1544             {\r
1545                 unsigned long entry_ofs;\r
1546                 unsigned long entry_siz;\r
1547                 size_t entry_name_len;\r
1548                 char namebuf[256];\r
1549                 char *xp;\r
1550                 size_t xi;\r
1551 \r
1552                 /* \r
1553                  *   Parse this index entry:\r
1554                  *   \r
1555                  *.    <uint32> address (as offset from the block base)\r
1556                  *.    <uint32> size (in bytes)\r
1557                  *.    <uint8> name-length\r
1558                  *.    <byte * name-length> name (all bytes XORed with 0xFF)\r
1559                  */\r
1560                 entry_ofs = osrp4(p);\r
1561                 entry_siz = osrp4(p + 4);\r
1562                 entry_name_len = (unsigned char)p[8];\r
1563 \r
1564                 /* unmask the name */\r
1565                 memcpy(namebuf, p + 9, resname_len);\r
1566                 for (xi = resname_len, xp = namebuf ; xi != 0 ; --xi)\r
1567                     *xp++ ^= 0xFF;\r
1568 \r
1569                 /* if this is the one we're looking for, return it */\r
1570                 if (entry_name_len == resname_len\r
1571                     && my_memicmp(resname, namebuf, resname_len) == 0)\r
1572                 {\r
1573                     /* \r
1574                      *   fill in the return information - note that the entry\r
1575                      *   offset given in the header is an offset from data\r
1576                      *   block's starting location, so fix this up to an\r
1577                      *   absolute seek location for the return value \r
1578                      */\r
1579                     info->ptr = blockp + entry_ofs;\r
1580                     info->len = entry_siz;\r
1581 \r
1582                     /* return success */\r
1583                     return TRUE;\r
1584                 }\r
1585 \r
1586                 /* skip this entry (header + name length) */\r
1587                 p += 9 + entry_name_len;\r
1588             }\r
1589 \r
1590             /* \r
1591              *   if we got this far, we didn't find the name; so skip past\r
1592              *   the MRES section by adding the section length to the base\r
1593              *   pointer, and resume the main file scan \r
1594              */\r
1595             p = blockp + siz;\r
1596         }\r
1597         else if (memcmp(p, "EOF ", 4) == 0)\r
1598         {\r
1599             /* \r
1600              *   end of file - we've finished without finding the resource,\r
1601              *   so return failure \r
1602              */\r
1603             return FALSE;\r
1604         }\r
1605         else\r
1606         {\r
1607             /* \r
1608              *   we don't care about anything else - just skip this block and\r
1609              *   keep going; to skip the block, simply seek ahead past the\r
1610              *   block header and then past the block's contents, using the\r
1611              *   size given the in block header \r
1612              */\r
1613             p += siz + 10;\r
1614         }\r
1615     }\r
1616 \r
1617     /* \r
1618      *   reached EOF without an EOF marker - file must be corrupted; return\r
1619      *   'not found' \r
1620      */\r
1621     return FALSE;\r
1622 }\r
1623 \r
1624 /* ------------------------------------------------------------------------ */\r
1625 /*\r
1626  *   JPEG and PNG information extraction (based on the versions in\r
1627  *   babel_story_functions.c) \r
1628  */\r
1629 static int get_jpeg_dim(const void *img, int32 extent,\r
1630                         int32 *xout, int32 *yout)\r
1631 {\r
1632     const unsigned char *dp=(const unsigned char *) img;\r
1633     const unsigned char *ep=dp+extent;\r
1634     unsigned int t1, t2, w, h;\r
1635 \r
1636     t1 = *dp++;\r
1637     t2 = *dp++;\r
1638     if (t1 != 0xff || t2 != 0xD8)\r
1639         return FALSE;\r
1640 \r
1641     while(1)\r
1642     {\r
1643         if (dp>ep) return FALSE;\r
1644         for(t1=*(dp++);t1!=0xff;t1=*(dp++)) if (dp>ep) return FALSE;\r
1645         do { t1=*(dp++); if (dp>ep) return FALSE;} while (t1 == 0xff);\r
1646 \r
1647         if ((t1 & 0xF0) == 0xC0 && !(t1==0xC4 || t1==0xC8 || t1==0xCC))\r
1648         {\r
1649             dp+=3;\r
1650             if (dp>ep) return FALSE;\r
1651             h=*(dp++) << 8;\r
1652             if (dp>ep) return FALSE;\r
1653             h|=*(dp++);\r
1654             if (dp>ep) return FALSE;\r
1655             w=*(dp++) << 8;\r
1656             if (dp>ep) return FALSE;\r
1657             w|=*(dp);\r
1658 \r
1659             *xout = w;\r
1660             *yout = h;\r
1661             return TRUE;\r
1662         }\r
1663         else if (t1==0xD8 || t1==0xD9)\r
1664             break;\r
1665         else\r
1666         {\r
1667             int l;\r
1668 \r
1669             if (dp>ep) return FALSE;\r
1670             l=*(dp++) << 8;\r
1671             if (dp>ep) return FALSE;\r
1672             l|= *(dp++);\r
1673             l-=2;\r
1674             dp+=l;\r
1675             if (dp>ep) return FALSE;\r
1676         }\r
1677     }\r
1678     return FALSE;\r
1679 }\r
1680 \r
1681 static int32 png_read_int(const unsigned char *mem)\r
1682 {\r
1683     int32 i4 = mem[0],\r
1684     i3 = mem[1],\r
1685     i2 = mem[2],\r
1686     i1 = mem[3];\r
1687     return i1 | (i2<<8) | (i3<<16) | (i4<<24);\r
1688 }\r
1689 \r
1690 \r
1691 static int get_png_dim(const void *img, int32 extent,\r
1692                        int32 *xout, int32 *yout)\r
1693 {\r
1694     const unsigned char *dp=(const unsigned char *)img;\r
1695 \r
1696     if (extent<33 ||\r
1697         !(dp[0]==137 && dp[1]==80 && dp[2]==78 && dp[3]==71 &&\r
1698           dp[4]==13 && dp[5] == 10 && dp[6] == 26 && dp[7]==10)||\r
1699         !(dp[12]=='I' && dp[13]=='H' && dp[14]=='D' && dp[15]=='R'))\r
1700         return FALSE;\r
1701 \r
1702     *xout = png_read_int(dp+16);\r
1703     *yout = png_read_int(dp+20);\r
1704     return TRUE;\r
1705 }\r
1706 \r
1707 /* ------------------------------------------------------------------------ */\r
1708 /*\r
1709  *   Testing main() - this implements a set of unit tests on the tads\r
1710  *   version.  \r
1711  */\r
1712 \r
1713 #ifdef TADS_TEST\r
1714 \r
1715 #include "babel_handler.h"\r
1716 \r
1717 void main(int argc, char **argv)\r
1718 {\r
1719     FILE *fp;\r
1720     int32 siz;\r
1721     void *buf;\r
1722     valinfo *head;\r
1723     int32 rv;\r
1724     int tadsver;\r
1725     char outbuf[TREATY_MINIMUM_EXTENT];\r
1726 \r
1727     /* check arguments */\r
1728     if (argc != 2)\r
1729     {\r
1730         printf("usage: tads <game-file>\n");\r
1731         exit(1);\r
1732     }\r
1733 \r
1734     /* initialize the babel subsystems */\r
1735     babel_init(argv[1]);\r
1736 \r
1737     /* open the story file */\r
1738     if ((fp = fopen(argv[1], "rb")) == 0)\r
1739     {\r
1740         printf("error opening input file\n");\r
1741         exit(2);\r
1742     }\r
1743 \r
1744     /* check the file size */\r
1745     fseek(fp, 0, SEEK_END);\r
1746     siz = ftell(fp);\r
1747     fseek(fp, 0, SEEK_SET);\r
1748 \r
1749     /* allocate space for it */\r
1750     if ((buf = malloc(siz)) == 0)\r
1751     {\r
1752         printf("error allocating space to load file\n");\r
1753         exit(2);\r
1754     }\r
1755 \r
1756     /* load it */\r
1757     if ((int32)fread(buf, 1, siz, fp) != siz)\r
1758     {\r
1759         printf("error reading file\n");\r
1760         exit(2);\r
1761     }\r
1762 \r
1763     /* done with the file */\r
1764     fclose(fp);\r
1765 \r
1766 \r
1767 \r
1768     /* ===== test 1 - basic parse_game_info() test ===== */\r
1769 \r
1770     /* parse the gameinfo record and print the results */\r
1771     if ((head = parse_game_info(buf, siz, &tadsver)) != 0)\r
1772     {\r
1773         valinfo *val;\r
1774 \r
1775         printf("found GameInfo - tads major version = %d\n", tadsver);\r
1776         for (val = head ; val != 0 ; val = val->nxt)\r
1777         {\r
1778             printf("%.*s=[%.*s]\n",\r
1779                    (int)val->name_len, val->name,\r
1780                    (int)val->val_len, val->val);\r
1781         }\r
1782         printf("\n");\r
1783     }\r
1784     else\r
1785         printf("no GameInfo found\n\n");\r
1786 \r
1787 \r
1788 \r
1789     /* ===== test 2 - test the get_story_file_IFID generator ===== */\r
1790     rv = tads_get_story_file_IFID(buf, siz, outbuf, TREATY_MINIMUM_EXTENT);\r
1791     if (rv == 1)\r
1792         printf("IFID = [%s]\n\n", outbuf);\r
1793     else\r
1794         printf("IFID return code = %ld\n", rv);\r
1795 \r
1796 \r
1797 \r
1798     /* ===== test 3 - test the ifiction synthesizer ===== */\r
1799     if ((rv = tads_get_story_file_metadata_extent(buf, siz)) > 0)\r
1800     {\r
1801         char *ifbuf;\r
1802 \r
1803         /* try allocating the space */\r
1804         if ((ifbuf = malloc((size_t)rv)) != 0)\r
1805         {\r
1806             /* synthesize the story file */\r
1807             rv = tads_get_story_file_metadata(buf, siz, ifbuf, rv);\r
1808             if (rv > 0)\r
1809                 printf("ifiction metadata:\n=====\n%.*s\n=====\n\n",\r
1810                        (int)rv, ifbuf);\r
1811             else\r
1812                 printf("tads_get_story_file_metadata result = %ld\n", rv);\r
1813         }\r
1814         else\r
1815             printf("unable to allocate %ld bytes for metadata record\n", rv);\r
1816     }\r
1817     else\r
1818         printf("tads_get_story_file_metadata_extent result code = %ld\n", rv);\r
1819     \r
1820 \r
1821     /* free the loaded story file buffer */\r
1822     free(buf);\r
1823 }\r
1824 \r
1825 \r
1826 #endif TADS_TEST\r
1827 \r