2 * tads.c - Treaty of Babel common functions for tads2 and tads3 modules
\r
4 * This file depends on treaty_builder.h
\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
10 *. 04/08/2006 LRRaszewski - changed babel API calls to threadsafe versions
\r
11 *. 04/08/2006 MJRoberts - initial implementation
\r
23 #define ASSERT_OUTPUT_SIZE(x) \
\r
24 do { if (output_extent < (x)) return INVALID_USAGE_RV; } while (0)
\r
26 #define T2_SIGNATURE "TADS2 bin\012\015\032"
\r
27 #define T3_SIGNATURE "T3-image\015\012\032"
\r
36 /* ------------------------------------------------------------------------ */
\r
38 * private structures
\r
42 * resource information structure - this encapsulates the location and size
\r
43 * of a binary resource object embedded in a story file
\r
45 typedef struct resinfo resinfo;
\r
48 /* pointer and length of the data in the story file buffer */
\r
52 /* tads major version (currently, 2 or 3) */
\r
57 * Name/value pair list entry
\r
59 typedef struct valinfo valinfo;
\r
69 /* next entry in the list */
\r
74 /* ------------------------------------------------------------------------ */
\r
76 * forward declarations
\r
78 static valinfo *parse_game_info(const void *story_file, int32 story_len,
\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
103 /* ------------------------------------------------------------------------ */
\r
105 * Get the IFID for a given story file.
\r
107 int32 tads_get_story_file_IFID(void *story_file, int32 extent,
\r
108 char *output, int32 output_extent)
\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
118 /* find the "IFID" key */
\r
119 if ((val = find_by_key(vals, "IFID")) != 0)
\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
129 * count up the IFIDs in the buffer - there might be more than
\r
130 * one, separated by commas
\r
132 for (found = 1, p = output ; *p != '\0' ; ++p)
\r
134 /* if this is a comma, it delimits a new IFID */
\r
140 /* delete the GameInfo list */
\r
141 delete_valinfo_list(vals);
\r
143 /* if we found an IFID, indicate how many results we found */
\r
149 * we didn't find an IFID in the GameInfo, so generate a default IFID
\r
150 * using the MD5 method
\r
152 return generate_md5_ifid(story_file, extent, output, output_extent);
\r
156 * Get the size of the ifiction metadata for the game
\r
158 int32 tads_get_story_file_metadata_extent(void *story_file, int32 extent)
\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
168 if ((vals = parse_game_info(story_file, extent, &ver)) == 0)
\r
169 return NO_REPLY_RV;
\r
172 * Run the ifiction synthesizer with no output buffer, to calculate the
\r
175 ret = synth_ifiction(vals, ver, 0, 0, story_file, extent);
\r
177 /* delete the value list */
\r
178 delete_valinfo_list(vals);
\r
180 /* return the required size */
\r
185 * Get the ifiction metadata for the game
\r
187 int32 tads_get_story_file_metadata(void *story_file, int32 extent,
\r
188 char *buf, int32 bufsize)
\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
198 /* synthesize the ifiction data into the output buffer */
\r
199 ret = synth_ifiction(vals, ver, buf, bufsize, story_file, extent);
\r
201 /* if that required more space than we had available, return an error */
\r
203 ret = INVALID_USAGE_RV;
\r
205 /* delete the value list */
\r
206 delete_valinfo_list(vals);
\r
208 /* return the result */
\r
213 * Get the size of the cover art
\r
215 int32 tads_get_story_file_cover_extent(void *story_file, int32 story_len)
\r
219 /* look for the cover art resource */
\r
220 if (find_cover_art(story_file, story_len, &res, 0, 0, 0))
\r
223 return NO_REPLY_RV;
\r
227 * Get the format of the cover art
\r
229 int32 tads_get_story_file_cover_format(void *story_file, int32 story_len)
\r
233 /* look for CoverArt.jpg */
\r
234 if (find_cover_art(story_file, story_len, 0, &typ, 0, 0))
\r
237 return NO_REPLY_RV;
\r
241 * Get the cover art data
\r
243 int32 tads_get_story_file_cover(void *story_file, int32 story_len,
\r
244 void *outbuf, int32 output_extent)
\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
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
259 /* otherwise, we didn't find it */
\r
260 return NO_REPLY_RV;
\r
263 /* ------------------------------------------------------------------------ */
\r
265 * Generate a default IFID using the MD5 hash method
\r
267 static int32 generate_md5_ifid(void *story_file, int32 extent,
\r
268 char *output, int32 output_extent)
\r
271 unsigned char md5_buf[16];
\r
275 /* calculate the MD5 hash of the story file */
\r
277 md5_append(&md5, story_file, extent);
\r
278 md5_finish(&md5, md5_buf);
\r
280 /* make sure we have room to store the result */
\r
281 ASSERT_OUTPUT_SIZE(39);
\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
287 strcpy(output, "TADS3-");
\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
293 /* indicate that we found one result */
\r
297 /* ------------------------------------------------------------------------ */
\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
303 /* is c a space? */
\r
304 #define u_isspace(c) ((unsigned char)(c) < 128 && isspace(c))
\r
306 /* is c a horizontal space? */
\r
307 #define u_ishspace(c) (u_isspace(c) && (c) != '\n' && (c) != '\r')
\r
309 /* is-newline - matches \n, \r, and \u2028 */
\r
310 static int u_isnl(const char *p, int32 len)
\r
312 return (*p == '\n'
\r
315 && *(unsigned char *)p == 0xe2
\r
316 && *(unsigned char *)(p+1) == 0x80
\r
317 && *(unsigned char *)(p+2) == 0xa8));
\r
320 /* skip to the next utf-8 character */
\r
321 static void nextc(const char **p, int32 *len)
\r
323 /* skip the first byte */
\r
327 /* skip continuation bytes */
\r
328 while (*len != 0 && (**p & 0xC0) == 0x80)
\r
332 /* skip to the previous utf-8 character */
\r
333 static void prevc(const char **p, int32 *len)
\r
335 /* move back one byte */
\r
338 /* keep skipping as long as we're looking at continuation characters */
\r
339 while ((**p & 0xC0) == 0x80)
\r
344 * Skip a newline sequence. Skips all common conventions, including \n,
\r
345 * \r, \n\r, \r\n, and \u2028.
\r
347 static void skip_newline(const char **p, int32 *rem)
\r
349 /* make sure we have something to skip */
\r
353 /* check what we have */
\r
354 switch (**(const unsigned char **)p)
\r
357 /* skip \n or \n\r */
\r
364 /* skip \r or \r\n */
\r
371 /* \u2028 (unicode line separator) - just skip the one character */
\r
378 * Skip to the next line
\r
380 static void skip_to_next_line(const char **p, int32 *rem)
\r
382 /* look for the next newline */
\r
383 for ( ; *rem != 0 ; nextc(p, rem))
\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
388 /* skip the newline, and we're done */
\r
389 skip_newline(p, rem);
\r
396 /* ------------------------------------------------------------------------ */
\r
398 * ifiction synthesizer output context
\r
400 typedef struct synthctx synthctx;
\r
403 /* the current output pointer */
\r
406 /* the number of bytes remaining in the output buffer */
\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
416 /* the head of the name/value pair list from the parsed GameInfo */
\r
420 /* initialize a synthesizer context */
\r
421 static void init_synthctx(synthctx *ctx, char *buf, int32 bufsize,
\r
424 /* set up at the beginning of the output buffer */
\r
426 ctx->buf_size = bufsize;
\r
428 /* we haven't written anything to the output buffer yet */
\r
429 ctx->total_size = 0;
\r
431 /* remember the name/value pair list */
\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
440 static void write_ifiction(synthctx *ctx, const char *src, size_t srclen)
\r
444 /* copy as much as we can, up to the remaining buffer size */
\r
446 if (copy_len > ctx->buf_size)
\r
447 copy_len = ctx->buf_size;
\r
449 /* do the copying, if any */
\r
452 /* copy the bytes */
\r
453 memcpy(ctx->buf, src, (size_t)copy_len);
\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
460 /* count this source data in the total size */
\r
461 ctx->total_size += srclen;
\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
467 write_ifiction(ctx, src, strlen(src));
\r
471 * Write a PCDATA string to the synthesized ifiction record. In
\r
472 * particular, we rewrite '<', '>', and '&' as '<', '>', and '&',
\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
476 static void write_ifiction_pcdata(synthctx *ctx, const char *p, size_t len)
\r
478 /* first, skip any leading whitespace */
\r
479 for ( ; len != 0 && u_ishspace(*p) ; ++p, --len) ;
\r
481 /* keep going until we run out of string */
\r
486 /* scan to the next whitespace or markup-significant character */
\r
488 len != 0 && !u_ishspace(*p)
\r
489 && *p != '<' && *p != '>' && *p != '&' ; ++p, --len) ;
\r
491 /* write the part up to here */
\r
493 write_ifiction(ctx, start, p - start);
\r
495 /* if we've reached the end of the string, we can stop */
\r
499 /* check what stopped us */
\r
503 write_ifiction_z(ctx, "<");
\r
508 write_ifiction_z(ctx, ">");
\r
513 write_ifiction_z(ctx, "&");
\r
519 * The only other thing that could have stopped us is
\r
520 * whitespace. Skip all consecutive whitespace.
\r
522 for ( ; len != 0 && u_ishspace(*p) ; ++p, --len);
\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
531 write_ifiction_z(ctx, " ");
\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
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
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
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
557 const char *valstr;
\r
560 /* look up the GameInfo key */
\r
561 if ((val = find_by_key(ctx->vals, gameinfo_key)) != 0)
\r
563 /* we found the GameInfo value - use it */
\r
565 vallen = val->val_len;
\r
567 else if (dflt != 0)
\r
569 /* the GameInfo value doesn't exist, but we have a default - use it */
\r
571 vallen = strlen(dflt);
\r
575 /* there's no GameInfo value and no default, so write nothing */
\r
579 /* write the indentation */
\r
580 while (indent != 0)
\r
582 static const char spaces[] = " ";
\r
585 /* figure how much we can write on this round */
\r
587 if (cur > sizeof(spaces) - 1)
\r
588 cur = sizeof(spaces) - 1;
\r
591 write_ifiction(ctx, spaces, cur);
\r
593 /* deduct it from the amount remaining */
\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
602 /* write the value, applying pcdata translations */
\r
604 write_ifiction(ctx, valstr, vallen);
\r
606 write_ifiction_pcdata(ctx, valstr, vallen);
\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
614 #define write_ifiction_xlat(ctx, indent, gikey, iftag, dflt) \
\r
615 write_ifiction_xlat_base(ctx, indent, gikey, iftag, dflt, FALSE)
\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
622 * Retrieve the next author name from the GameInfo "Author" format. The
\r
623 * format is as follows:
\r
625 * name <email> <email>... ; ...
\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
631 static int scan_author_name(const char **p, size_t *len,
\r
632 const char **start, const char **end)
\r
634 /* keep going until we find a non-empty author name */
\r
637 /* skip leading spaces */
\r
638 for ( ; *len != 0 && u_ishspace(**p) ; ++*p, --*len) ;
\r
640 /* if we ran out of string, there's definitely no author name */
\r
645 * Find the end of this author name. The author name ends at the
\r
646 * next semicolon or angle bracket.
\r
648 for (*start = *p ; *len != 0 && **p != ';' && **p != '<' ;
\r
651 /* trim off any trailing spaces */
\r
652 for (*end = *p ; *end > *start && u_ishspace(*(*end - 1)) ; --*end) ;
\r
654 /* now skip any email addresses */
\r
655 while (*len != 0 && **p == '<')
\r
657 /* skip to the closing bracket */
\r
658 for (++*p, --*len ; *len != 0 && **p != '>' ; ++*p, --*len) ;
\r
660 /* skip the bracket */
\r
664 /* skip whitespace */
\r
665 for ( ; *len != 0 && u_ishspace(**p) ; ++*p, --*len) ;
\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
671 if (*len != 0 && **p != '<' && **p != ';')
\r
678 /* if we're at a semicolon, skip it */
\r
679 if (*len != 0 && **p == ';')
\r
683 * if we found a non-empty name, return it; otherwise, continue on
\r
684 * to the next semicolon section
\r
686 if (*end != *start)
\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
700 static int32 synth_ifiction(valinfo *vals, int tads_version,
\r
701 char *buf, int32 bufsize,
\r
702 void *story_file, int32 extent)
\r
704 char default_ifid[TREATY_MINIMUM_EXTENT];
\r
705 valinfo *ifid = find_by_key(vals, "IFID");
\r
706 const char *ifid_val;
\r
708 valinfo *author = find_by_key(vals, "AuthorEmail");
\r
709 valinfo *url = find_by_key(vals, "Url");
\r
714 int32 art_wid, art_ht;
\r
716 /* initialize the output content */
\r
717 init_synthctx(&ctx, buf, bufsize, vals);
\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
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
730 /* use the explicit IFID(s) listed in the GameInfo */
\r
731 ifid_val = ifid->val;
\r
732 ifid_len = ifid->val_len;
\r
736 /* generate the default IFID */
\r
737 generate_md5_ifid(story_file, extent,
\r
738 default_ifid, TREATY_MINIMUM_EXTENT);
\r
740 /* use this as the IFID */
\r
741 ifid_val = default_ifid;
\r
742 ifid_len = strlen(default_ifid);
\r
745 /* write the header, and start the <identification> section */
\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
754 " <generator>Babel</generator>\n"
\r
755 " <generatorversion>" TREATY_VERSION "</generatorversion>\n"
\r
756 " <originated>2006-04-14</originated>\n"
\r
758 " <identification>\n");
\r
760 /* write each IFID (there might be several) */
\r
761 for (p = ifid_val, rem = ifid_len ; rem != 0 ; )
\r
766 /* skip leading spaces */
\r
767 for ( ; rem != 0 && u_ishspace(*p) ; ++p, --rem) ;
\r
769 /* find the end of this IFID */
\r
770 for (start = p ; rem != 0 && *p != ',' ; ++p, --rem) ;
\r
772 /* remove trailing spaces */
\r
773 for (end = p ; end > start && u_ishspace(*(end-1)) ; --end) ;
\r
775 /* if we found one, write it out */
\r
778 write_ifiction_z(&ctx, " <ifid>");
\r
779 write_ifiction(&ctx, start, end - start);
\r
780 write_ifiction_z(&ctx, "</ifid>\n");
\r
783 /* skip the comma */
\r
784 if (rem != 0 && *p == ',')
\r
788 /* add the format information */
\r
789 write_ifiction_z(&ctx,
\r
791 ? " <format>tads2</format>\n"
\r
792 : " <format>tads3</format>\n");
\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
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
810 /* if there's an author, write the list of author names */
\r
818 /* start the <author> tag */
\r
819 write_ifiction_z(&ctx, " <author>");
\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
825 for (p = author->val, rem = author->val_len, cnt = 1 ;
\r
826 scan_author_name(&p, &rem, &start, &end) ; ) ;
\r
829 * Now generate the list of authors. If there are multiple
\r
830 * authors, use commas to separate them.
\r
832 for (p = author->val, rem = author->val_len, i = 0 ; ; ++i)
\r
834 /* scan this author's name */
\r
835 if (!scan_author_name(&p, &rem, &start, &end))
\r
838 /* write out this author name */
\r
839 write_ifiction_pcdata(&ctx, start, end - start);
\r
841 /* if there's another name to come, write a separator */
\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
849 write_ifiction_z(&ctx,
\r
850 cnt == 2 ? " and " :
\r
851 i + 2 < cnt ? ", " : ", and ");
\r
855 /* end the <author> tag */
\r
856 write_ifiction_z(&ctx, "</author>\n");
\r
859 /* end the biblio section */
\r
860 write_ifiction_z(&ctx, " </bibliographic>\n");
\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
870 " <format>%s</format>\n"
\r
871 " <height>%lu</height>\n"
\r
872 " <width>%lu</width>\n"
\r
874 art_fmt == PNG_COVER_FORMAT ? "png" : "jpg",
\r
875 (long)art_ht, (long)art_wid);
\r
877 write_ifiction_z(&ctx, buf);
\r
880 /* if there's an author email, include it */
\r
881 if (author != 0 || url != 0)
\r
887 /* open the section */
\r
888 write_ifiction_z(&ctx, " <contacts>\n");
\r
890 /* add the author email, if provided */
\r
893 /* write the email list */
\r
894 for (i = 0, p = author->val, rem = author->val_len ; ; ++i)
\r
898 /* skip to the next email address */
\r
899 for ( ; rem != 0 && *p != '<' ; ++p, --rem) ;
\r
901 /* if we didn't find an email address, we're done */
\r
905 /* find the matching '>' */
\r
906 for (++p, --rem, start = p ; rem != 0 && *p != '>' ;
\r
910 * if this is the first one, open the section; otherwise,
\r
914 write_ifiction_z(&ctx, " <authoremail>");
\r
916 write_ifiction_z(&ctx, ",");
\r
918 /* write this address */
\r
919 write_ifiction(&ctx, start, p - start);
\r
922 * skip the closing bracket, if there is one; if we're out
\r
923 * of string, we're done
\r
931 /* if we found any emails to write, end the section */
\r
933 write_ifiction_z(&ctx, "</authoremail>\n");
\r
936 /* if there's a URL, add it */
\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
944 /* close the section */
\r
945 write_ifiction_z(&ctx, " </contacts>\n");
\r
948 /* add the tads-specific section */
\r
949 write_ifiction_z(&ctx, " <tads>\n");
\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
957 write_ifiction_z(&ctx, " </tads>\n");
\r
959 /* close the story section and the main body */
\r
960 write_ifiction_z(&ctx, " </story>\n</ifindex>\n");
\r
962 /* add the null terminator */
\r
963 write_ifiction(&ctx, "", 1);
\r
965 /* return the total output size */
\r
966 return ctx.total_size;
\r
969 /* ------------------------------------------------------------------------ */
\r
971 * Check a data block to see if it starts with the given signature.
\r
973 int tads_match_sig(const void *buf, int32 len, const char *sig)
\r
975 /* note the length of the signature string */
\r
976 size_t sig_len = strlen(sig);
\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
983 /* ------------------------------------------------------------------------ */
\r
985 * portable-to-native format conversions
\r
987 #define osbyte(p, ofs) \
\r
988 (*(((unsigned char *)(p)) + (ofs)))
\r
991 ((unsigned int)osbyte(p, 0))
\r
994 ((unsigned int)osbyte(p, 0) \
\r
995 + ((unsigned int)osbyte(p, 1) << 8))
\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
1004 /* ------------------------------------------------------------------------ */
\r
1006 * Parse a game file and retrieve the GameInfo data. Returns the head of a
\r
1007 * linked list of valinfo entries.
\r
1009 static valinfo *parse_game_info(const void *story_file, int32 story_len,
\r
1010 int *tads_version)
\r
1015 valinfo *val_head = 0;
\r
1018 * first, find the GameInfo resource - if it's not there, there's no
\r
1019 * game information to parse
\r
1021 if (!find_resource(story_file, story_len, "GameInfo.txt", &res))
\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
1028 /* parse the data */
\r
1029 for (p = res.ptr, rem = res.len ; rem != 0 ; )
\r
1031 const char *name_start;
\r
1033 const char *val_start;
\r
1039 /* skip any leading whitespace */
\r
1040 while (rem != 0 && u_isspace(*p))
\r
1043 /* if the line starts with '#', it's a comment, so skip it */
\r
1044 if (rem != 0 && *p == '#')
\r
1046 skip_to_next_line(&p, &rem);
\r
1050 /* we must have the start of a name - note it */
\r
1053 /* skip ahead to a space or colon */
\r
1054 while (rem != 0 && *p != ':' && !u_ishspace(*p))
\r
1057 /* note the length of the name */
\r
1058 name_len = p - name_start;
\r
1060 /* skip any whitespace before the presumed colon */
\r
1061 while (rem != 0 && u_ishspace(*p))
\r
1064 /* if we're not at a colon, the line is ill-formed, so skip it */
\r
1065 if (rem == 0 || *p != ':')
\r
1067 /* skip the entire line, and go back for the next one */
\r
1068 skip_to_next_line(&p, &rem);
\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
1075 /* note where the value starts */
\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
1087 /* skip to the next line */
\r
1088 skip_to_next_line(&p, &rem);
\r
1090 /* if we're at eof, we can stop now */
\r
1094 /* note where this line starts */
\r
1099 * if we're at a non-whitespace character, it's definitely not
\r
1100 * a continuation line
\r
1102 if (!u_ishspace(*p))
\r
1106 * check for spaces followed by a non-space character - this
\r
1107 * would signify a continuation line
\r
1109 for ( ; rem != 0 && u_ishspace(*p) ; nextc(&p, &rem)) ;
\r
1110 if (rem == 0 || u_isnl(p, rem))
\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
1123 * We found whitespace followed by non-whitespace, so this is a
\r
1124 * continuation line. Keep going for now.
\r
1128 /* remove any trailing newlines */
\r
1129 while (p > val_start)
\r
1131 /* move back one character */
\r
1135 * if it's a newline, keep going; otherwise, keep this
\r
1136 * character and stop trimming
\r
1138 if (!u_isnl(p, rem))
\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
1153 val = (valinfo *)malloc(sizeof(valinfo) + (p - val_start));
\r
1155 /* link it into our list */
\r
1156 val->nxt = val_head;
\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
1163 /* point the value to the space allocated along with the valinfo */
\r
1164 val->val = (char *)(val + 1);
\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
1172 /* find the next newline */
\r
1173 for (l = inp ; inlen != 0 && !u_isnl(inp, inlen) ;
\r
1174 nextc(&inp, &inlen)) ;
\r
1176 /* copy this line to the output */
\r
1177 memcpy(outp, l, inp - l);
\r
1180 /* if we're out of input, we're done */
\r
1184 /* we're at a newline: replace it with a space in the output */
\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
1192 /* set the length of the parsed value */
\r
1193 val->val_len = outp - val->val;
\r
1195 /* skip to the next line and continue parsing */
\r
1196 skip_to_next_line(&p, &rem);
\r
1199 /* return the head of the linked list of value entries */
\r
1202 static int my_memicmp(const void *aa, const void *bb, int l)
\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
1212 /* ------------------------------------------------------------------------ */
\r
1214 * Given a valinfo list obtained from parse_game_info(), find the value for
\r
1217 static valinfo *find_by_key(valinfo *list_head, const char *key)
\r
1220 size_t key_len = strlen(key);
\r
1222 /* scan the list for the given key */
\r
1223 for (p = list_head ; p != 0 ; p = p->nxt)
\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
1234 /* ------------------------------------------------------------------------ */
\r
1236 * Delete a valinfo list obtained from parse_game_info()
\r
1238 static void delete_valinfo_list(valinfo *head)
\r
1240 /* keep going until we run out of entries */
\r
1243 /* remember the next entry, before we delete this one */
\r
1244 valinfo *nxt = head->nxt;
\r
1246 /* delete this one */
\r
1249 /* move on to the next one */
\r
1254 /* ------------------------------------------------------------------------ */
\r
1256 * Find the cover art resource. We'll look for CoverArt.jpg and
\r
1257 * CoverArt.png, in that order.
\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
1266 /* if they didn't want the resource info, provide a placeholder */
\r
1270 /* look for CoverArt.jpg first */
\r
1271 if (find_resource(story_file, story_len, "CoverArt.jpg", resp))
\r
1273 /* get the width and height */
\r
1274 if (!get_jpeg_dim(resp->ptr, resp->len, &x, &y))
\r
1277 /* hand back the width and height if it was requested */
\r
1283 /* tell them it's a JPEG image */
\r
1284 if (image_format != 0)
\r
1285 *image_format = JPEG_COVER_FORMAT;
\r
1287 /* indicate success */
\r
1291 /* look for CoverArt.png second */
\r
1292 if (find_resource(story_file, story_len, "CoverArt.png", resp))
\r
1294 /* get the width and height */
\r
1295 if (!get_png_dim(resp->ptr, resp->len, &x, &y))
\r
1298 /* hand back the width and height if it was requested */
\r
1304 /* tell them it's a PNG image */
\r
1305 if (image_format != 0)
\r
1306 *image_format = PNG_COVER_FORMAT;
\r
1308 /* indicate success */
\r
1312 /* didn't find it */
\r
1316 /* ------------------------------------------------------------------------ */
\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
1322 static int find_resource(const void *story_file, int32 story_len,
\r
1323 const char *resname, resinfo *info)
\r
1325 /* if there's no file, there's no resource */
\r
1326 if (story_file == 0)
\r
1329 /* check for tads 2 */
\r
1330 if (tads_match_sig(story_file, story_len, T2_SIGNATURE))
\r
1332 info->tads_version = 2;
\r
1333 return t2_find_res(story_file, story_len, resname, info);
\r
1336 /* check for tads 3 */
\r
1337 if (tads_match_sig(story_file, story_len, T3_SIGNATURE))
\r
1339 info->tads_version = 3;
\r
1340 return t3_find_res(story_file, story_len, resname, info);
\r
1343 /* it's not one of ours */
\r
1347 /* ------------------------------------------------------------------------ */
\r
1349 * Find a resource in a tads 2 game file
\r
1351 static int t2_find_res(const void *story_file, int32 story_len,
\r
1352 const char *resname, resinfo *info)
\r
1354 const char *basep = (const char *)story_file;
\r
1355 const char *endp = basep + story_len;
\r
1357 size_t resname_len;
\r
1359 /* note the length of the name we're seeking */
\r
1360 resname_len = strlen(resname);
\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
1367 p = basep + 13 + 7 + 2 + 26;
\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
1375 unsigned long endofs;
\r
1378 * We're pointing to a section block header, which looks like this:
\r
1380 *. <byte> type-length
\r
1381 *. <byte * type-length> type-name
\r
1382 *. <uint32> next-section-address
\r
1385 /* read the ending offset */
\r
1386 endofs = osrp4(p + 1 + osrp1(p));
\r
1388 /* check the type */
\r
1389 if (p[0] == 7 && memcmp(p + 1, "HTMLRES", 7) == 0)
\r
1391 unsigned long found_ofs;
\r
1393 unsigned long entry_cnt;
\r
1395 /* we haven't found the resource yet */
\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
1405 entry_cnt = osrp4(p);
\r
1407 /* skip to the first index entry */
\r
1410 /* scan the index entries */
\r
1411 for ( ; entry_cnt != 0 ; --entry_cnt)
\r
1413 unsigned long res_ofs;
\r
1414 unsigned long res_siz;
\r
1418 * We're at the next index entry, which looks like this:
\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
1425 res_ofs = osrp4(p);
\r
1426 res_siz = osrp4(p + 4);
\r
1427 name_len = osrp2(p + 8);
\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
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
1441 found_ofs = res_ofs;
\r
1442 info->len = res_siz;
\r
1445 /* skip this one's name */
\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
1457 /* fix up the offset with the actual file location */
\r
1458 info->ptr = p + found_ofs;
\r
1460 /* tell the caller we found it */
\r
1464 else if (p[0] == 4 && memcmp(p + 1, "$EOF", 4) == 0)
\r
1467 * that's the end of the file - we've finished without finding
\r
1468 * the resource, so return failure
\r
1473 /* move to the next section */
\r
1474 p = basep + endofs;
\r
1478 * reached EOF without an $EOF marker - file must be corrupted; return
\r
1484 /* ------------------------------------------------------------------------ */
\r
1486 * Find a resource in a T3 image file
\r
1488 static int t3_find_res(const void *story_file, int32 story_len,
\r
1489 const char *resname, resinfo *info)
\r
1491 const char *basep = (const char *)story_file;
\r
1492 const char *endp = basep + story_len;
\r
1494 size_t resname_len;
\r
1496 /* note the length of the name we're seeking */
\r
1497 resname_len = strlen(resname);
\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
1503 p = basep + 11 + 2 + 32 + 24;
\r
1505 /* scan the data blocks */
\r
1508 unsigned long siz;
\r
1511 * We're at the next block header, which looks like this:
\r
1513 *. <byte * 4> type-name
\r
1514 *. <uint32> block-size
\r
1518 /* get the block size */
\r
1519 siz = osrp4(p + 4);
\r
1521 /* check the type */
\r
1522 if (memcmp(p, "MRES", 4) == 0)
\r
1524 unsigned int entry_cnt;
\r
1526 const char *blockp;
\r
1528 /* skip the header */
\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
1538 /* the first thing in the table is the number of entries */
\r
1539 entry_cnt = osrp2(p);
\r
1542 /* read the entries */
\r
1543 for (i = 0 ; i < entry_cnt ; ++i)
\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
1553 * Parse this index entry:
\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
1560 entry_ofs = osrp4(p);
\r
1561 entry_siz = osrp4(p + 4);
\r
1562 entry_name_len = (unsigned char)p[8];
\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
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
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
1579 info->ptr = blockp + entry_ofs;
\r
1580 info->len = entry_siz;
\r
1582 /* return success */
\r
1586 /* skip this entry (header + name length) */
\r
1587 p += 9 + entry_name_len;
\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
1597 else if (memcmp(p, "EOF ", 4) == 0)
\r
1600 * end of file - we've finished without finding the resource,
\r
1601 * so return failure
\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
1618 * reached EOF without an EOF marker - file must be corrupted; return
\r
1624 /* ------------------------------------------------------------------------ */
\r
1626 * JPEG and PNG information extraction (based on the versions in
\r
1627 * babel_story_functions.c)
\r
1629 static int get_jpeg_dim(const void *img, int32 extent,
\r
1630 int32 *xout, int32 *yout)
\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
1638 if (t1 != 0xff || t2 != 0xD8)
\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
1647 if ((t1 & 0xF0) == 0xC0 && !(t1==0xC4 || t1==0xC8 || t1==0xCC))
\r
1650 if (dp>ep) return FALSE;
\r
1652 if (dp>ep) return FALSE;
\r
1654 if (dp>ep) return FALSE;
\r
1656 if (dp>ep) return FALSE;
\r
1663 else if (t1==0xD8 || t1==0xD9)
\r
1669 if (dp>ep) return FALSE;
\r
1671 if (dp>ep) return FALSE;
\r
1675 if (dp>ep) return FALSE;
\r
1681 static int32 png_read_int(const unsigned char *mem)
\r
1683 int32 i4 = mem[0],
\r
1687 return i1 | (i2<<8) | (i3<<16) | (i4<<24);
\r
1691 static int get_png_dim(const void *img, int32 extent,
\r
1692 int32 *xout, int32 *yout)
\r
1694 const unsigned char *dp=(const unsigned char *)img;
\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
1702 *xout = png_read_int(dp+16);
\r
1703 *yout = png_read_int(dp+20);
\r
1707 /* ------------------------------------------------------------------------ */
\r
1709 * Testing main() - this implements a set of unit tests on the tads
\r
1715 #include "babel_handler.h"
\r
1717 void main(int argc, char **argv)
\r
1725 char outbuf[TREATY_MINIMUM_EXTENT];
\r
1727 /* check arguments */
\r
1730 printf("usage: tads <game-file>\n");
\r
1734 /* initialize the babel subsystems */
\r
1735 babel_init(argv[1]);
\r
1737 /* open the story file */
\r
1738 if ((fp = fopen(argv[1], "rb")) == 0)
\r
1740 printf("error opening input file\n");
\r
1744 /* check the file size */
\r
1745 fseek(fp, 0, SEEK_END);
\r
1747 fseek(fp, 0, SEEK_SET);
\r
1749 /* allocate space for it */
\r
1750 if ((buf = malloc(siz)) == 0)
\r
1752 printf("error allocating space to load file\n");
\r
1757 if ((int32)fread(buf, 1, siz, fp) != siz)
\r
1759 printf("error reading file\n");
\r
1763 /* done with the file */
\r
1768 /* ===== test 1 - basic parse_game_info() test ===== */
\r
1770 /* parse the gameinfo record and print the results */
\r
1771 if ((head = parse_game_info(buf, siz, &tadsver)) != 0)
\r
1775 printf("found GameInfo - tads major version = %d\n", tadsver);
\r
1776 for (val = head ; val != 0 ; val = val->nxt)
\r
1778 printf("%.*s=[%.*s]\n",
\r
1779 (int)val->name_len, val->name,
\r
1780 (int)val->val_len, val->val);
\r
1785 printf("no GameInfo found\n\n");
\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
1792 printf("IFID = [%s]\n\n", outbuf);
\r
1794 printf("IFID return code = %ld\n", rv);
\r
1798 /* ===== test 3 - test the ifiction synthesizer ===== */
\r
1799 if ((rv = tads_get_story_file_metadata_extent(buf, siz)) > 0)
\r
1803 /* try allocating the space */
\r
1804 if ((ifbuf = malloc((size_t)rv)) != 0)
\r
1806 /* synthesize the story file */
\r
1807 rv = tads_get_story_file_metadata(buf, siz, ifbuf, rv);
\r
1809 printf("ifiction metadata:\n=====\n%.*s\n=====\n\n",
\r
1812 printf("tads_get_story_file_metadata result = %ld\n", rv);
\r
1815 printf("unable to allocate %ld bytes for metadata record\n", rv);
\r
1818 printf("tads_get_story_file_metadata_extent result code = %ld\n", rv);
\r
1821 /* free the loaded story file buffer */
\r