+/* \r
+ * tads.c - Treaty of Babel common functions for tads2 and tads3 modules\r
+ * \r
+ * This file depends on treaty_builder.h\r
+ * \r
+ * This file is public domain, but note that any changes to this file may\r
+ * render it noncompliant with the Treaty of Babel\r
+ * \r
+ * Modified\r
+ *. 04/08/2006 LRRaszewski - changed babel API calls to threadsafe versions\r
+ *. 04/08/2006 MJRoberts - initial implementation\r
+ */\r
+\r
+\r
+#include "treaty.h"\r
+#include <ctype.h>\r
+#include <stdio.h>\r
+#include <string.h>\r
+#include <stdlib.h>\r
+#include "tads.h"\r
+#include "md5.h"\r
+\r
+#define ASSERT_OUTPUT_SIZE(x) \\r
+ do { if (output_extent < (x)) return INVALID_USAGE_RV; } while (0)\r
+\r
+#define T2_SIGNATURE "TADS2 bin\012\015\032"\r
+#define T3_SIGNATURE "T3-image\015\012\032"\r
+\r
+#ifndef FALSE\r
+#define FALSE 0\r
+#endif\r
+#ifndef TRUE\r
+#define TRUE 1\r
+#endif\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * private structures \r
+ */\r
+\r
+/*\r
+ * resource information structure - this encapsulates the location and size\r
+ * of a binary resource object embedded in a story file \r
+ */\r
+typedef struct resinfo resinfo;\r
+struct resinfo\r
+{\r
+ /* pointer and length of the data in the story file buffer */\r
+ const char *ptr;\r
+ int32 len;\r
+\r
+ /* tads major version (currently, 2 or 3) */\r
+ int tads_version;\r
+};\r
+\r
+/*\r
+ * Name/value pair list entry \r
+ */\r
+typedef struct valinfo valinfo;\r
+struct valinfo\r
+{\r
+ const char *name;\r
+ size_t name_len;\r
+\r
+ /* value string */\r
+ char *val;\r
+ size_t val_len;\r
+\r
+ /* next entry in the list */\r
+ valinfo *nxt;\r
+};\r
+\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * forward declarations \r
+ */\r
+static valinfo *parse_game_info(const void *story_file, int32 story_len,\r
+ int *version);\r
+static int find_resource(const void *story_file, int32 story_len,\r
+ const char *resname, resinfo *info);\r
+static int find_cover_art(const void *story_file, int32 story_len,\r
+ resinfo *resp, int32 *image_format,\r
+ int32 *width, int32 *height);\r
+static int t2_find_res(const void *story_file, int32 story_len,\r
+ const char *resname, resinfo *info);\r
+static int t3_find_res(const void *story_file, int32 story_len,\r
+ const char *resname, resinfo *info);\r
+static valinfo *find_by_key(valinfo *list_head, const char *key);\r
+static void delete_valinfo_list(valinfo *head);\r
+static int32 generate_md5_ifid(void *story_file, int32 extent,\r
+ char *output, int32 output_extent);\r
+static int32 synth_ifiction(valinfo *vals, int tads_version,\r
+ char *buf, int32 bufsize,\r
+ void *story_file, int32 extent);\r
+static int get_png_dim(const void *img, int32 extent,\r
+ int32 *xout, int32 *yout);\r
+static int get_jpeg_dim(const void *img, int32 extent,\r
+ int32 *xout, int32 *yout);\r
+\r
+\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Get the IFID for a given story file. \r
+ */\r
+int32 tads_get_story_file_IFID(void *story_file, int32 extent,\r
+ char *output, int32 output_extent)\r
+{\r
+ valinfo *vals;\r
+ \r
+ /* if we have GameInfo, try looking for an IFID there */\r
+ if ((vals = parse_game_info(story_file, extent, 0)) != 0)\r
+ {\r
+ valinfo *val;\r
+ int found = 0;\r
+ \r
+ /* find the "IFID" key */\r
+ if ((val = find_by_key(vals, "IFID")) != 0)\r
+ {\r
+ char *p;\r
+ \r
+ /* copy the output as a null-terminated string */\r
+ ASSERT_OUTPUT_SIZE((int32)val->val_len + 1);\r
+ memcpy(output, val->val, val->val_len);\r
+ output[val->val_len] = '\0';\r
+\r
+ /* \r
+ * count up the IFIDs in the buffer - there might be more than\r
+ * one, separated by commas \r
+ */\r
+ for (found = 1, p = output ; *p != '\0' ; ++p)\r
+ {\r
+ /* if this is a comma, it delimits a new IFID */\r
+ if (*p == ',')\r
+ ++found;\r
+ }\r
+ }\r
+\r
+ /* delete the GameInfo list */\r
+ delete_valinfo_list(vals);\r
+\r
+ /* if we found an IFID, indicate how many results we found */\r
+ if (found != 0)\r
+ return found;\r
+ }\r
+\r
+ /* \r
+ * we didn't find an IFID in the GameInfo, so generate a default IFID\r
+ * using the MD5 method \r
+ */\r
+ return generate_md5_ifid(story_file, extent, output, output_extent);\r
+}\r
+\r
+/*\r
+ * Get the size of the ifiction metadata for the game \r
+ */\r
+int32 tads_get_story_file_metadata_extent(void *story_file, int32 extent)\r
+{\r
+ valinfo *vals;\r
+ int32 ret;\r
+ int ver;\r
+ \r
+ /*\r
+ * First, make sure we have a GameInfo record. If we don't, simply\r
+ * indicate that there's no metadata to fetch. \r
+ */\r
+ if ((vals = parse_game_info(story_file, extent, &ver)) == 0)\r
+ return NO_REPLY_RV;\r
+\r
+ /*\r
+ * Run the ifiction synthesizer with no output buffer, to calculate the\r
+ * size we need. \r
+ */\r
+ ret = synth_ifiction(vals, ver, 0, 0, story_file, extent);\r
+\r
+ /* delete the value list */\r
+ delete_valinfo_list(vals);\r
+\r
+ /* return the required size */\r
+ return ret;\r
+}\r
+\r
+/*\r
+ * Get the ifiction metadata for the game\r
+ */\r
+int32 tads_get_story_file_metadata(void *story_file, int32 extent,\r
+ char *buf, int32 bufsize)\r
+{\r
+ valinfo *vals;\r
+ int32 ret;\r
+ int ver;\r
+\r
+ /* make sure we have metadata to fetch */\r
+ if ((vals = parse_game_info(story_file, extent, &ver)) == 0)\r
+ return NO_REPLY_RV;\r
+\r
+ /* synthesize the ifiction data into the output buffer */\r
+ ret = synth_ifiction(vals, ver, buf, bufsize, story_file, extent);\r
+\r
+ /* if that required more space than we had available, return an error */\r
+ if (ret > bufsize)\r
+ ret = INVALID_USAGE_RV;\r
+\r
+ /* delete the value list */\r
+ delete_valinfo_list(vals);\r
+\r
+ /* return the result */\r
+ return ret;\r
+}\r
+\r
+/*\r
+ * Get the size of the cover art \r
+ */\r
+int32 tads_get_story_file_cover_extent(void *story_file, int32 story_len)\r
+{\r
+ resinfo res;\r
+ \r
+ /* look for the cover art resource */\r
+ if (find_cover_art(story_file, story_len, &res, 0, 0, 0))\r
+ return res.len;\r
+ else\r
+ return NO_REPLY_RV;\r
+}\r
+\r
+/*\r
+ * Get the format of the cover art \r
+ */\r
+int32 tads_get_story_file_cover_format(void *story_file, int32 story_len)\r
+{\r
+ int32 typ;\r
+\r
+ /* look for CoverArt.jpg */\r
+ if (find_cover_art(story_file, story_len, 0, &typ, 0, 0))\r
+ return typ;\r
+ else\r
+ return NO_REPLY_RV;\r
+}\r
+\r
+/*\r
+ * Get the cover art data \r
+ */\r
+int32 tads_get_story_file_cover(void *story_file, int32 story_len,\r
+ void *outbuf, int32 output_extent)\r
+{\r
+ resinfo res;\r
+\r
+ /* look for CoverArt.jpg, then for CoverArt.png */\r
+ if (find_cover_art(story_file, story_len, &res, 0, 0, 0))\r
+ {\r
+ /* got it - copy the data to the buffer */\r
+ ASSERT_OUTPUT_SIZE(res.len);\r
+ memcpy(outbuf, res.ptr, res.len);\r
+\r
+ /* success */\r
+ return res.len;\r
+ }\r
+\r
+ /* otherwise, we didn't find it */\r
+ return NO_REPLY_RV;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Generate a default IFID using the MD5 hash method \r
+ */\r
+static int32 generate_md5_ifid(void *story_file, int32 extent,\r
+ char *output, int32 output_extent)\r
+{\r
+ md5_state_t md5;\r
+ unsigned char md5_buf[16];\r
+ char *p;\r
+ int i;\r
+\r
+ /* calculate the MD5 hash of the story file */\r
+ md5_init(&md5);\r
+ md5_append(&md5, story_file, extent);\r
+ md5_finish(&md5, md5_buf);\r
+\r
+ /* make sure we have room to store the result */\r
+ ASSERT_OUTPUT_SIZE(39);\r
+\r
+ /* the prefix is "TADS2-" or "TADS3-", depending on the format */\r
+ if (tads_match_sig(story_file, extent, T2_SIGNATURE))\r
+ strcpy(output, "TADS2-");\r
+ else\r
+ strcpy(output, "TADS3-");\r
+\r
+ /* the rest is the MD5 hash of the file, as hex digits */\r
+ for (i = 0, p = output + strlen(output) ; i < 16 ; p += 2, ++i)\r
+ sprintf(p, "%02X", md5_buf[i]);\r
+\r
+ /* indicate that we found one result */\r
+ return 1;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Some UTF-8 utility functions and macros. We use our own rather than the\r
+ * ctype.h macros because we're parsing UTF-8 text. \r
+ */\r
+\r
+/* is c a space? */\r
+#define u_isspace(c) ((unsigned char)(c) < 128 && isspace(c))\r
+\r
+/* is c a horizontal space? */\r
+#define u_ishspace(c) (u_isspace(c) && (c) != '\n' && (c) != '\r')\r
+\r
+/* is-newline - matches \n, \r, and \u2028 */\r
+static int u_isnl(const char *p, int32 len)\r
+{\r
+ return (*p == '\n' \r
+ || *p == '\r'\r
+ || (len >= 3\r
+ && *(unsigned char *)p == 0xe2\r
+ && *(unsigned char *)(p+1) == 0x80\r
+ && *(unsigned char *)(p+2) == 0xa8));\r
+}\r
+\r
+/* skip to the next utf-8 character */\r
+static void nextc(const char **p, int32 *len)\r
+{\r
+ /* skip the first byte */\r
+ if (*len != 0)\r
+ ++*p, --*len;\r
+\r
+ /* skip continuation bytes */\r
+ while (*len != 0 && (**p & 0xC0) == 0x80)\r
+ ++*p, --*len;\r
+}\r
+\r
+/* skip to the previous utf-8 character */\r
+static void prevc(const char **p, int32 *len)\r
+{\r
+ /* move back one byte */\r
+ --*p, ++*len;\r
+\r
+ /* keep skipping as long as we're looking at continuation characters */\r
+ while ((**p & 0xC0) == 0x80)\r
+ --*p, ++*len;\r
+}\r
+\r
+/*\r
+ * Skip a newline sequence. Skips all common conventions, including \n,\r
+ * \r, \n\r, \r\n, and \u2028. \r
+ */\r
+static void skip_newline(const char **p, int32 *rem)\r
+{\r
+ /* make sure we have something to skip */\r
+ if (*rem == 0)\r
+ return;\r
+\r
+ /* check what we have */\r
+ switch (**(const unsigned char **)p)\r
+ {\r
+ case '\n':\r
+ /* skip \n or \n\r */\r
+ nextc(p, rem);\r
+ if (**p == '\r')\r
+ nextc(p, rem);\r
+ break;\r
+\r
+ case '\r':\r
+ /* skip \r or \r\n */\r
+ nextc(p, rem);\r
+ if (**p == '\n')\r
+ nextc(p, rem);\r
+ break;\r
+\r
+ case 0xe2:\r
+ /* \u2028 (unicode line separator) - just skip the one character */\r
+ nextc(p, rem);\r
+ break;\r
+ }\r
+}\r
+\r
+/*\r
+ * Skip to the next line \r
+ */\r
+static void skip_to_next_line(const char **p, int32 *rem)\r
+{\r
+ /* look for the next newline */\r
+ for ( ; *rem != 0 ; nextc(p, rem))\r
+ {\r
+ /* if this is a newline of some kind, we're at the end of the line */\r
+ if (u_isnl(*p, *rem))\r
+ {\r
+ /* skip the newline, and we're done */\r
+ skip_newline(p, rem);\r
+ break;\r
+ }\r
+ }\r
+}\r
+\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * ifiction synthesizer output context \r
+ */\r
+typedef struct synthctx synthctx;\r
+struct synthctx\r
+{\r
+ /* the current output pointer */\r
+ char *buf;\r
+\r
+ /* the number of bytes remaining in the output buffer */\r
+ int32 buf_size;\r
+\r
+ /* \r
+ * the total number of bytes needed for the output (this might be more\r
+ * than we've actually written, since we count up the bytes required\r
+ * even if we need more space than the buffer provides) \r
+ */\r
+ int32 total_size;\r
+\r
+ /* the head of the name/value pair list from the parsed GameInfo */\r
+ valinfo *vals;\r
+};\r
+\r
+/* initialize a synthesizer context */\r
+static void init_synthctx(synthctx *ctx, char *buf, int32 bufsize,\r
+ valinfo *vals)\r
+{\r
+ /* set up at the beginning of the output buffer */\r
+ ctx->buf = buf;\r
+ ctx->buf_size = bufsize;\r
+\r
+ /* we haven't written anything to the output buffer yet */\r
+ ctx->total_size = 0;\r
+\r
+ /* remember the name/value pair list */\r
+ ctx->vals = vals;\r
+}\r
+\r
+/* \r
+ * Write out a chunk to a synthesized ifiction record, updating pointers\r
+ * and counters. We won't copy past the end of the buffer, but we'll\r
+ * continue counting the output length needed in any case. \r
+ */\r
+static void write_ifiction(synthctx *ctx, const char *src, size_t srclen)\r
+{\r
+ int32 copy_len;\r
+\r
+ /* copy as much as we can, up to the remaining buffer size */\r
+ copy_len = srclen;\r
+ if (copy_len > ctx->buf_size)\r
+ copy_len = ctx->buf_size;\r
+\r
+ /* do the copying, if any */\r
+ if (copy_len != 0)\r
+ {\r
+ /* copy the bytes */\r
+ memcpy(ctx->buf, src, (size_t)copy_len);\r
+\r
+ /* adjust the buffer pointer and output buffer size remaining */\r
+ ctx->buf += copy_len;\r
+ ctx->buf_size -= copy_len;\r
+ }\r
+\r
+ /* count this source data in the total size */\r
+ ctx->total_size += srclen;\r
+}\r
+\r
+/* write a null-terminated chunk to the synthesized ifiction record */\r
+static void write_ifiction_z(synthctx *ctx, const char *src)\r
+{\r
+ write_ifiction(ctx, src, strlen(src));\r
+}\r
+\r
+/*\r
+ * Write a PCDATA string to the synthesized ifiction record. In\r
+ * particular, we rewrite '<', '>', and '&' as '<', '>', and '&',\r
+ * respectively; we trim off leading and trailing spaces; and we compress\r
+ * each run of whitespace down to a single \u0020 (' ') character.\r
+ */\r
+static void write_ifiction_pcdata(synthctx *ctx, const char *p, size_t len)\r
+{\r
+ /* first, skip any leading whitespace */\r
+ for ( ; len != 0 && u_ishspace(*p) ; ++p, --len) ;\r
+\r
+ /* keep going until we run out of string */\r
+ for (;;)\r
+ {\r
+ const char *start;\r
+ \r
+ /* scan to the next whitespace or markup-significant character */\r
+ for (start = p ;\r
+ len != 0 && !u_ishspace(*p)\r
+ && *p != '<' && *p != '>' && *p != '&' ; ++p, --len) ;\r
+\r
+ /* write the part up to here */\r
+ if (p != start)\r
+ write_ifiction(ctx, start, p - start);\r
+\r
+ /* if we've reached the end of the string, we can stop */\r
+ if (len == 0)\r
+ break;\r
+\r
+ /* check what stopped us */\r
+ switch (*p)\r
+ {\r
+ case '<':\r
+ write_ifiction_z(ctx, "<");\r
+ ++p, --len;\r
+ break;\r
+\r
+ case '>':\r
+ write_ifiction_z(ctx, ">");\r
+ ++p, --len;\r
+ break;\r
+\r
+ case '&':\r
+ write_ifiction_z(ctx, "&");\r
+ ++p, --len;\r
+ break;\r
+\r
+ default:\r
+ /* \r
+ * The only other thing that could have stopped us is\r
+ * whitespace. Skip all consecutive whitespace. \r
+ */\r
+ for ( ; len != 0 && u_ishspace(*p) ; ++p, --len);\r
+\r
+ /* \r
+ * if that's not the end of the string, replace the run of\r
+ * whitespace with a single space character in the output; if\r
+ * we've reached the end of the string, we don't even want to\r
+ * do that, since we want to trim off trailing spaces \r
+ */\r
+ if (len != 0)\r
+ write_ifiction_z(ctx, " ");\r
+ break;\r
+ }\r
+ }\r
+}\r
+\r
+/*\r
+ * Translate a GameInfo keyed value to the corresponding ifiction tagged\r
+ * value. We find the GameInfo value keyed by 'gameinfo_key', and write\r
+ * out the same string under the ifiction XML tag 'ifiction_tag'. We write\r
+ * a complete XML container sequence - <tag>value</tag>.\r
+ * \r
+ * If the given GameInfo key doesn't exist, we use the default value string\r
+ * 'dflt', if given. If the GameInfo key doesn't exist and 'dflt' is null,\r
+ * we don't write anything - we don't even write the open/close tags.\r
+ * \r
+ * If 'html' is true, we assume the value is in html format, and we write\r
+ * it untranslated. Otherwise, we write it as PCDATA, translating markup\r
+ * characters into '&' entities and compressing whitespace. \r
+ */\r
+static void write_ifiction_xlat_base(synthctx *ctx, int indent,\r
+ const char *gameinfo_key,\r
+ const char *ifiction_tag,\r
+ const char *dflt, int html)\r
+{\r
+ valinfo *val;\r
+ const char *valstr;\r
+ size_t vallen;\r
+ \r
+ /* look up the GameInfo key */\r
+ if ((val = find_by_key(ctx->vals, gameinfo_key)) != 0)\r
+ {\r
+ /* we found the GameInfo value - use it */\r
+ valstr = val->val;\r
+ vallen = val->val_len;\r
+ }\r
+ else if (dflt != 0)\r
+ {\r
+ /* the GameInfo value doesn't exist, but we have a default - use it */\r
+ valstr = dflt;\r
+ vallen = strlen(dflt);\r
+ }\r
+ else\r
+ {\r
+ /* there's no GameInfo value and no default, so write nothing */\r
+ return;\r
+ }\r
+\r
+ /* write the indentation */\r
+ while (indent != 0)\r
+ {\r
+ static const char spaces[] = " ";\r
+ size_t cur;\r
+\r
+ /* figure how much we can write on this round */\r
+ cur = indent;\r
+ if (cur > sizeof(spaces) - 1)\r
+ cur = sizeof(spaces) - 1;\r
+\r
+ /* write it */\r
+ write_ifiction(ctx, spaces, cur);\r
+\r
+ /* deduct it from the amount remaining */\r
+ indent -= cur;\r
+ }\r
+\r
+ /* write the open tag */\r
+ write_ifiction_z(ctx, "<");\r
+ write_ifiction_z(ctx, ifiction_tag);\r
+ write_ifiction_z(ctx, ">");\r
+\r
+ /* write the value, applying pcdata translations */\r
+ if (html)\r
+ write_ifiction(ctx, valstr, vallen);\r
+ else\r
+ write_ifiction_pcdata(ctx, valstr, vallen);\r
+\r
+ /* write the close tag */\r
+ write_ifiction_z(ctx, "</");\r
+ write_ifiction_z(ctx, ifiction_tag);\r
+ write_ifiction_z(ctx, ">\n");\r
+}\r
+\r
+#define write_ifiction_xlat(ctx, indent, gikey, iftag, dflt) \\r
+ write_ifiction_xlat_base(ctx, indent, gikey, iftag, dflt, FALSE)\r
+\r
+#define write_ifiction_xlat_html(ctx, indent, gikey, iftag, dflt) \\r
+ write_ifiction_xlat_base(ctx, indent, gikey, iftag, dflt, TRUE)\r
+\r
+\r
+/*\r
+ * Retrieve the next author name from the GameInfo "Author" format. The\r
+ * format is as follows:\r
+ * \r
+ * name <email> <email>... ; ...\r
+ * \r
+ * That is, each author is listed with a name followed by one or more email\r
+ * addresses in angle brackets, and multiple authors are separated by\r
+ * semicolons. \r
+ */\r
+static int scan_author_name(const char **p, size_t *len,\r
+ const char **start, const char **end)\r
+{\r
+ /* keep going until we find a non-empty author name */\r
+ for (;;)\r
+ {\r
+ /* skip leading spaces */\r
+ for ( ; *len != 0 && u_ishspace(**p) ; ++*p, --*len) ;\r
+\r
+ /* if we ran out of string, there's definitely no author name */\r
+ if (*len == 0)\r
+ return FALSE;\r
+\r
+ /* \r
+ * Find the end of this author name. The author name ends at the\r
+ * next semicolon or angle bracket. \r
+ */\r
+ for (*start = *p ; *len != 0 && **p != ';' && **p != '<' ;\r
+ ++*p, --*len) ;\r
+\r
+ /* trim off any trailing spaces */\r
+ for (*end = *p ; *end > *start && u_ishspace(*(*end - 1)) ; --*end) ;\r
+\r
+ /* now skip any email addresses */\r
+ while (*len != 0 && **p == '<')\r
+ {\r
+ /* skip to the closing bracket */\r
+ for (++*p, --*len ; *len != 0 && **p != '>' ; ++*p, --*len) ;\r
+\r
+ /* skip the bracket */\r
+ if (*len != 0)\r
+ ++*p, --*len;\r
+\r
+ /* skip whitespace */\r
+ for ( ; *len != 0 && u_ishspace(**p) ; ++*p, --*len) ;\r
+\r
+ /* \r
+ * if we're not at a semicolon, another angle bracket, or the\r
+ * end of the string, it's a syntax error \r
+ */\r
+ if (*len != 0 && **p != '<' && **p != ';')\r
+ {\r
+ *len = 0;\r
+ return FALSE;\r
+ }\r
+ }\r
+\r
+ /* if we're at a semicolon, skip it */\r
+ if (*len != 0 && **p == ';')\r
+ ++*p, --*len;\r
+\r
+ /* \r
+ * if we found a non-empty name, return it; otherwise, continue on\r
+ * to the next semicolon section \r
+ */\r
+ if (*end != *start)\r
+ return TRUE;\r
+ }\r
+}\r
+\r
+\r
+/*\r
+ * Synthesize an ifiction record for the given GameInfo name/value pair\r
+ * list. Returns the number of bytes required for the result, including\r
+ * null termination. We'll copy as much as we can to the output buffer, up\r
+ * to bufsize; if the buffer size is insufficient to hold the result, we'll\r
+ * still indicate the length needed for the full result, but we're careful\r
+ * not to actually copy anything past the end of the buffer. \r
+ */\r
+static int32 synth_ifiction(valinfo *vals, int tads_version,\r
+ char *buf, int32 bufsize,\r
+ void *story_file, int32 extent)\r
+{\r
+ char default_ifid[TREATY_MINIMUM_EXTENT];\r
+ valinfo *ifid = find_by_key(vals, "IFID");\r
+ const char *ifid_val;\r
+ size_t ifid_len;\r
+ valinfo *author = find_by_key(vals, "AuthorEmail");\r
+ valinfo *url = find_by_key(vals, "Url");\r
+ synthctx ctx;\r
+ const char *p;\r
+ size_t rem;\r
+ int32 art_fmt;\r
+ int32 art_wid, art_ht;\r
+\r
+ /* initialize the output content */\r
+ init_synthctx(&ctx, buf, bufsize, vals);\r
+\r
+ /* make sure the tads version is one we know how to handle */\r
+ if (tads_version != 2 && tads_version != 3)\r
+ return NO_REPLY_RV;\r
+\r
+ /* \r
+ * The IFID is mandatory. If there's not an IFID specifically listed\r
+ * in the GameInfo, we need to generate the default IFID based on the\r
+ * MD5 hash of the game file. \r
+ */\r
+ if (ifid != 0)\r
+ {\r
+ /* use the explicit IFID(s) listed in the GameInfo */\r
+ ifid_val = ifid->val;\r
+ ifid_len = ifid->val_len;\r
+ }\r
+ else\r
+ {\r
+ /* generate the default IFID */\r
+ generate_md5_ifid(story_file, extent,\r
+ default_ifid, TREATY_MINIMUM_EXTENT);\r
+\r
+ /* use this as the IFID */\r
+ ifid_val = default_ifid;\r
+ ifid_len = strlen(default_ifid);\r
+ }\r
+\r
+ /* write the header, and start the <identification> section */\r
+ write_ifiction_z(\r
+ &ctx,\r
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"\r
+ "<ifindex version=\"1.0\" "\r
+ "xmlns=\"http://babel.ifarchive.org/protocol/iFiction/\">\n"\r
+ " <!-- Bibliographic data translated from TADS GameInfo -->\n"\r
+ " <story>\n"\r
+ " <colophon>\n"\r
+ " <generator>Babel</generator>\n"\r
+ " <generatorversion>" TREATY_VERSION "</generatorversion>\n"\r
+ " <originated>2006-04-14</originated>\n"\r
+ " </colophon>\n"\r
+ " <identification>\n");\r
+\r
+ /* write each IFID (there might be several) */\r
+ for (p = ifid_val, rem = ifid_len ; rem != 0 ; )\r
+ {\r
+ const char *start;\r
+ const char *end;\r
+\r
+ /* skip leading spaces */\r
+ for ( ; rem != 0 && u_ishspace(*p) ; ++p, --rem) ;\r
+ \r
+ /* find the end of this IFID */\r
+ for (start = p ; rem != 0 && *p != ',' ; ++p, --rem) ;\r
+\r
+ /* remove trailing spaces */\r
+ for (end = p ; end > start && u_ishspace(*(end-1)) ; --end) ;\r
+\r
+ /* if we found one, write it out */\r
+ if (end != start)\r
+ {\r
+ write_ifiction_z(&ctx, " <ifid>");\r
+ write_ifiction(&ctx, start, end - start);\r
+ write_ifiction_z(&ctx, "</ifid>\n");\r
+ }\r
+\r
+ /* skip the comma */\r
+ if (rem != 0 && *p == ',')\r
+ ++p, --rem;\r
+ }\r
+\r
+ /* add the format information */\r
+ write_ifiction_z(&ctx,\r
+ tads_version == 2\r
+ ? " <format>tads2</format>\n"\r
+ : " <format>tads3</format>\n");\r
+\r
+ /* close the <identification> section and start the <bibliographic> */\r
+ write_ifiction_z(&ctx,\r
+ " </identification>\n"\r
+ " <bibliographic>\n");\r
+\r
+ /* write the various bibliographic data */\r
+ write_ifiction_xlat(&ctx, 6, "Name", "title", "An Interactive Fiction");\r
+ write_ifiction_xlat(&ctx, 6, "Headline", "headline", 0);\r
+ write_ifiction_xlat(&ctx, 6, "Desc", "description", 0);\r
+ write_ifiction_xlat(&ctx, 6, "Genre", "genre", 0);\r
+ write_ifiction_xlat(&ctx, 6, "Forgiveness", "forgiveness", 0);\r
+ write_ifiction_xlat(&ctx, 6, "Series", "series", 0);\r
+ write_ifiction_xlat(&ctx, 6, "SeriesNumber", "seriesnumber", 0);\r
+ write_ifiction_xlat(&ctx, 6, "Language", "language", 0);\r
+ write_ifiction_xlat(&ctx, 6, "FirstPublished", "firstpublished", 0);\r
+\r
+ /* if there's an author, write the list of author names */\r
+ if (author != 0)\r
+ {\r
+ int cnt;\r
+ int i;\r
+ const char *start;\r
+ const char *end;\r
+\r
+ /* start the <author> tag */\r
+ write_ifiction_z(&ctx, " <author>");\r
+ \r
+ /* \r
+ * first, count up the number of authors - authors are separated by\r
+ * semicolons, so there's one more author than there are semicolons\r
+ */\r
+ for (p = author->val, rem = author->val_len, cnt = 1 ;\r
+ scan_author_name(&p, &rem, &start, &end) ; ) ;\r
+\r
+ /* \r
+ * Now generate the list of authors. If there are multiple\r
+ * authors, use commas to separate them. \r
+ */\r
+ for (p = author->val, rem = author->val_len, i = 0 ; ; ++i)\r
+ {\r
+ /* scan this author's name */\r
+ if (!scan_author_name(&p, &rem, &start, &end))\r
+ break;\r
+ \r
+ /* write out this author name */\r
+ write_ifiction_pcdata(&ctx, start, end - start);\r
+\r
+ /* if there's another name to come, write a separator */\r
+ if (i + 1 < cnt)\r
+ {\r
+ /* \r
+ * write just "and" to separate two items; write ","\r
+ * between items in lists of more than two, with ",and"\r
+ * between the last two items \r
+ */\r
+ write_ifiction_z(&ctx,\r
+ cnt == 2 ? " and " :\r
+ i + 2 < cnt ? ", " : ", and ");\r
+ }\r
+ }\r
+\r
+ /* end the <author> tag */\r
+ write_ifiction_z(&ctx, "</author>\n");\r
+ }\r
+\r
+ /* end the biblio section */\r
+ write_ifiction_z(&ctx, " </bibliographic>\n");\r
+\r
+ /* if there's cover art, add its information */\r
+ if (find_cover_art(story_file, extent, 0, &art_fmt, &art_wid, &art_ht)\r
+ && (art_fmt == PNG_COVER_FORMAT || art_fmt == JPEG_COVER_FORMAT))\r
+ {\r
+ char buf[200];\r
+ \r
+ sprintf(buf,\r
+ " <cover>\n"\r
+ " <format>%s</format>\n"\r
+ " <height>%lu</height>\n"\r
+ " <width>%lu</width>\n"\r
+ " </cover>\n",\r
+ art_fmt == PNG_COVER_FORMAT ? "png" : "jpg",\r
+ (long)art_ht, (long)art_wid);\r
+\r
+ write_ifiction_z(&ctx, buf);\r
+ }\r
+\r
+ /* if there's an author email, include it */\r
+ if (author != 0 || url != 0)\r
+ {\r
+ const char *p;\r
+ size_t rem;\r
+ int i;\r
+ \r
+ /* open the section */\r
+ write_ifiction_z(&ctx, " <contacts>\n");\r
+\r
+ /* add the author email, if provided */\r
+ if (author != 0)\r
+ {\r
+ /* write the email list */\r
+ for (i = 0, p = author->val, rem = author->val_len ; ; ++i)\r
+ {\r
+ const char *start;\r
+ \r
+ /* skip to the next email address */\r
+ for ( ; rem != 0 && *p != '<' ; ++p, --rem) ;\r
+ \r
+ /* if we didn't find an email address, we're done */\r
+ if (rem == 0)\r
+ break;\r
+ \r
+ /* find the matching '>' */\r
+ for (++p, --rem, start = p ; rem != 0 && *p != '>' ;\r
+ ++p, --rem) ;\r
+\r
+ /* \r
+ * if this is the first one, open the section; otherwise,\r
+ * add a comma \r
+ */\r
+ if (i == 0)\r
+ write_ifiction_z(&ctx, " <authoremail>");\r
+ else\r
+ write_ifiction_z(&ctx, ",");\r
+ \r
+ /* write this address */\r
+ write_ifiction(&ctx, start, p - start);\r
+ \r
+ /* \r
+ * skip the closing bracket, if there is one; if we're out\r
+ * of string, we're done \r
+ */\r
+ if (rem != 0)\r
+ ++p, --rem;\r
+ else\r
+ break;\r
+ }\r
+\r
+ /* if we found any emails to write, end the section */\r
+ if (i != 0)\r
+ write_ifiction_z(&ctx, "</authoremail>\n");\r
+ }\r
+\r
+ /* if there's a URL, add it */\r
+ if (url != 0)\r
+ {\r
+ write_ifiction_z(&ctx, " <url>");\r
+ write_ifiction(&ctx, url->val, url->val_len);\r
+ write_ifiction_z(&ctx, "</url>\n");\r
+ }\r
+\r
+ /* close the section */\r
+ write_ifiction_z(&ctx, " </contacts>\n");\r
+ }\r
+\r
+ /* add the tads-specific section */\r
+ write_ifiction_z(&ctx, " <tads>\n");\r
+ \r
+ write_ifiction_xlat(&ctx, 6, "Version", "version", 0);\r
+ write_ifiction_xlat(&ctx, 6, "ReleaseDate", "releasedate", 0);\r
+ write_ifiction_xlat(&ctx, 6, "PresentationProfile",\r
+ "presentationprofile", 0);\r
+ write_ifiction_xlat(&ctx, 6, "Byline", "byline", 0);\r
+\r
+ write_ifiction_z(&ctx, " </tads>\n");\r
+\r
+ /* close the story section and the main body */\r
+ write_ifiction_z(&ctx, " </story>\n</ifindex>\n");\r
+ \r
+ /* add the null terminator */\r
+ write_ifiction(&ctx, "", 1);\r
+\r
+ /* return the total output size */\r
+ return ctx.total_size;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Check a data block to see if it starts with the given signature. \r
+ */\r
+int tads_match_sig(const void *buf, int32 len, const char *sig)\r
+{\r
+ /* note the length of the signature string */\r
+ size_t sig_len = strlen(sig);\r
+ \r
+ /* if matches if the buffer starts with the signature string */\r
+ return (len >= (int32)sig_len && memcmp(buf, sig, sig_len) == 0);\r
+}\r
+\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * portable-to-native format conversions \r
+ */\r
+#define osbyte(p, ofs) \\r
+ (*(((unsigned char *)(p)) + (ofs)))\r
+\r
+#define osrp1(p) \\r
+ ((unsigned int)osbyte(p, 0))\r
+\r
+#define osrp2(p) \\r
+ ((unsigned int)osbyte(p, 0) \\r
+ + ((unsigned int)osbyte(p, 1) << 8))\r
+\r
+#define osrp4(p) \\r
+ (((unsigned long)osbyte(p, 0)) \\r
+ + (((unsigned long)osbyte(p, 1)) << 8) \\r
+ + (((unsigned long)osbyte(p, 2)) << 16) \\r
+ + (((unsigned long)osbyte(p, 3)) << 24))\r
+\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Parse a game file and retrieve the GameInfo data. Returns the head of a\r
+ * linked list of valinfo entries.\r
+ */\r
+static valinfo *parse_game_info(const void *story_file, int32 story_len,\r
+ int *tads_version)\r
+{\r
+ resinfo res;\r
+ const char *p;\r
+ int32 rem;\r
+ valinfo *val_head = 0;\r
+\r
+ /* \r
+ * first, find the GameInfo resource - if it's not there, there's no\r
+ * game information to parse \r
+ */\r
+ if (!find_resource(story_file, story_len, "GameInfo.txt", &res))\r
+ return 0;\r
+\r
+ /* if the caller wants the TADS version number, hand it back */\r
+ if (tads_version != 0)\r
+ *tads_version = res.tads_version;\r
+\r
+ /* parse the data */\r
+ for (p = res.ptr, rem = res.len ; rem != 0 ; )\r
+ {\r
+ const char *name_start;\r
+ size_t name_len;\r
+ const char *val_start;\r
+ valinfo *val;\r
+ const char *inp;\r
+ int32 inlen;\r
+ char *outp;\r
+\r
+ /* skip any leading whitespace */\r
+ while (rem != 0 && u_isspace(*p))\r
+ ++p, --rem;\r
+\r
+ /* if the line starts with '#', it's a comment, so skip it */\r
+ if (rem != 0 && *p == '#')\r
+ {\r
+ skip_to_next_line(&p, &rem);\r
+ continue;\r
+ }\r
+\r
+ /* we must have the start of a name - note it */\r
+ name_start = p;\r
+\r
+ /* skip ahead to a space or colon */\r
+ while (rem != 0 && *p != ':' && !u_ishspace(*p))\r
+ nextc(&p, &rem);\r
+\r
+ /* note the length of the name */\r
+ name_len = p - name_start;\r
+\r
+ /* skip any whitespace before the presumed colon */\r
+ while (rem != 0 && u_ishspace(*p))\r
+ nextc(&p, &rem);\r
+\r
+ /* if we're not at a colon, the line is ill-formed, so skip it */\r
+ if (rem == 0 || *p != ':')\r
+ {\r
+ /* skip the entire line, and go back for the next one */\r
+ skip_to_next_line(&p, &rem);\r
+ continue;\r
+ }\r
+\r
+ /* skip the colon and any whitespace immediately after it */\r
+ for (nextc(&p, &rem) ; rem != 0 && u_ishspace(*p) ; nextc(&p, &rem)) ;\r
+\r
+ /* note where the value starts */\r
+ val_start = p;\r
+\r
+ /*\r
+ * Scan the value to get its length. The value runs from here to\r
+ * the next newline that's not followed immediately by a space. \r
+ */\r
+ while (rem != 0)\r
+ {\r
+ const char *nl;\r
+ int32 nlrem;\r
+ \r
+ /* skip to the next line */\r
+ skip_to_next_line(&p, &rem);\r
+\r
+ /* if we're at eof, we can stop now */\r
+ if (rem == 0)\r
+ break;\r
+\r
+ /* note where this line starts */\r
+ nl = p;\r
+ nlrem = rem;\r
+\r
+ /* \r
+ * if we're at a non-whitespace character, it's definitely not\r
+ * a continuation line \r
+ */\r
+ if (!u_ishspace(*p))\r
+ break;\r
+\r
+ /* \r
+ * check for spaces followed by a non-space character - this\r
+ * would signify a continuation line\r
+ */\r
+ for ( ; rem != 0 && u_ishspace(*p) ; nextc(&p, &rem)) ;\r
+ if (rem == 0 || u_isnl(p, rem))\r
+ {\r
+ /* \r
+ * we're at end of file, we found a line with nothing but\r
+ * whitespace, so this isn't a continuation line; go back\r
+ * to the start of this line and end the value here \r
+ */\r
+ p = nl;\r
+ rem = nlrem;\r
+ break;\r
+ }\r
+\r
+ /* \r
+ * We found whitespace followed by non-whitespace, so this is a\r
+ * continuation line. Keep going for now.\r
+ */\r
+ }\r
+\r
+ /* remove any trailing newlines */\r
+ while (p > val_start)\r
+ {\r
+ /* move back one character */\r
+ prevc(&p, &rem);\r
+\r
+ /* \r
+ * if it's a newline, keep going; otherwise, keep this\r
+ * character and stop trimming \r
+ */\r
+ if (!u_isnl(p, rem))\r
+ {\r
+ nextc(&p, &rem);\r
+ break;\r
+ }\r
+ }\r
+\r
+ /* \r
+ * Allocate a new value entry. Make room for the entry itself plus\r
+ * a copy of the value. We don't need to make a copy of the name,\r
+ * since we can just use the original copy from the story file\r
+ * buffer. We do need a copy of the value because we might need to\r
+ * transform it slightly, to remove newlines and leading spaces on\r
+ * continuation lines. \r
+ */\r
+ val = (valinfo *)malloc(sizeof(valinfo) + (p - val_start));\r
+\r
+ /* link it into our list */\r
+ val->nxt = val_head;\r
+ val_head = val;\r
+\r
+ /* point the name directly to the name in the buffer */\r
+ val->name = name_start;\r
+ val->name_len = name_len;\r
+\r
+ /* point the value to the space allocated along with the valinfo */\r
+ val->val = (char *)(val + 1);\r
+\r
+ /* store the name, removing newlines and continuation-line spaces */\r
+ for (outp = val->val, inp = val_start, inlen = p - val_start ;\r
+ inlen != 0 ; )\r
+ {\r
+ const char *l;\r
+ \r
+ /* find the next newline */\r
+ for (l = inp ; inlen != 0 && !u_isnl(inp, inlen) ;\r
+ nextc(&inp, &inlen)) ;\r
+\r
+ /* copy this line to the output */\r
+ memcpy(outp, l, inp - l);\r
+ outp += inp - l;\r
+\r
+ /* if we're out of input, we're done */\r
+ if (inlen == 0)\r
+ break;\r
+\r
+ /* we're at a newline: replace it with a space in the output */\r
+ *outp++ = ' ';\r
+\r
+ /* skip the newline and subsequent whitespace in the input */\r
+ for (skip_newline(&inp, &inlen) ;\r
+ inlen != 0 && u_ishspace(*inp) ; nextc(&inp, &inlen)) ;\r
+ }\r
+\r
+ /* set the length of the parsed value */\r
+ val->val_len = outp - val->val;\r
+\r
+ /* skip to the next line and continue parsing */\r
+ skip_to_next_line(&p, &rem);\r
+ }\r
+\r
+ /* return the head of the linked list of value entries */\r
+ return val_head;\r
+}\r
+static int my_memicmp(const void *aa, const void *bb, int l)\r
+{\r
+ int s=0,i;\r
+ char *a=(char *) aa;\r
+ char *b=(char *) bb;\r
+ for(i=0;i<l && !s;i++)\r
+ s=tolower(a[i])-tolower(b[i]);\r
+ return s;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Given a valinfo list obtained from parse_game_info(), find the value for\r
+ * the given key \r
+ */\r
+static valinfo *find_by_key(valinfo *list_head, const char *key)\r
+{\r
+ valinfo *p;\r
+ size_t key_len = strlen(key);\r
+ \r
+ /* scan the list for the given key */\r
+ for (p = list_head ; p != 0 ; p = p->nxt)\r
+ {\r
+ /* if this one matches the key we're looking for, return it */\r
+ if (p->name_len == key_len && my_memicmp(p->name, key, key_len) == 0)\r
+ return p;\r
+ }\r
+\r
+ /* no luck */\r
+ return 0;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Delete a valinfo list obtained from parse_game_info() \r
+ */\r
+static void delete_valinfo_list(valinfo *head)\r
+{\r
+ /* keep going until we run out of entries */\r
+ while (head != 0)\r
+ {\r
+ /* remember the next entry, before we delete this one */\r
+ valinfo *nxt = head->nxt;\r
+\r
+ /* delete this one */\r
+ free(head);\r
+\r
+ /* move on to the next one */\r
+ head = nxt;\r
+ }\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Find the cover art resource. We'll look for CoverArt.jpg and\r
+ * CoverArt.png, in that order. \r
+ */\r
+static int find_cover_art(const void *story_file, int32 story_len,\r
+ resinfo *resp, int32 *image_format,\r
+ int32 *width, int32 *height)\r
+{\r
+ resinfo res;\r
+ int32 x, y;\r
+\r
+ /* if they didn't want the resource info, provide a placeholder */\r
+ if (resp == 0)\r
+ resp = &res;\r
+\r
+ /* look for CoverArt.jpg first */\r
+ if (find_resource(story_file, story_len, "CoverArt.jpg", resp))\r
+ {\r
+ /* get the width and height */\r
+ if (!get_jpeg_dim(resp->ptr, resp->len, &x, &y))\r
+ return FALSE;\r
+\r
+ /* hand back the width and height if it was requested */\r
+ if (width != 0)\r
+ *width = x;\r
+ if (height != 0)\r
+ *height = y;\r
+\r
+ /* tell them it's a JPEG image */\r
+ if (image_format != 0)\r
+ *image_format = JPEG_COVER_FORMAT;\r
+\r
+ /* indicate success */\r
+ return TRUE;\r
+ }\r
+\r
+ /* look for CoverArt.png second */\r
+ if (find_resource(story_file, story_len, "CoverArt.png", resp))\r
+ {\r
+ /* get the width and height */\r
+ if (!get_png_dim(resp->ptr, resp->len, &x, &y))\r
+ return FALSE;\r
+\r
+ /* hand back the width and height if it was requested */\r
+ if (width != 0)\r
+ *width = x;\r
+ if (height != 0)\r
+ *height = y;\r
+\r
+ /* tell them it's a PNG image */\r
+ if (image_format != 0)\r
+ *image_format = PNG_COVER_FORMAT;\r
+\r
+ /* indicate success */\r
+ return TRUE;\r
+ }\r
+\r
+ /* didn't find it */\r
+ return FALSE;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Find a resource in a TADS 2 or 3 story file that's been loaded into\r
+ * memory. On success, fills in the offset and size of the resource and\r
+ * returns TRUE; if the resource isn't found, returns FALSE.\r
+ */\r
+static int find_resource(const void *story_file, int32 story_len,\r
+ const char *resname, resinfo *info)\r
+{\r
+ /* if there's no file, there's no resource */\r
+ if (story_file == 0)\r
+ return FALSE;\r
+\r
+ /* check for tads 2 */\r
+ if (tads_match_sig(story_file, story_len, T2_SIGNATURE))\r
+ {\r
+ info->tads_version = 2;\r
+ return t2_find_res(story_file, story_len, resname, info);\r
+ }\r
+\r
+ /* check for tads 3 */\r
+ if (tads_match_sig(story_file, story_len, T3_SIGNATURE))\r
+ {\r
+ info->tads_version = 3;\r
+ return t3_find_res(story_file, story_len, resname, info);\r
+ }\r
+\r
+ /* it's not one of ours */\r
+ return FALSE;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Find a resource in a tads 2 game file \r
+ */\r
+static int t2_find_res(const void *story_file, int32 story_len,\r
+ const char *resname, resinfo *info)\r
+{\r
+ const char *basep = (const char *)story_file;\r
+ const char *endp = basep + story_len;\r
+ const char *p;\r
+ size_t resname_len;\r
+\r
+ /* note the length of the name we're seeking */\r
+ resname_len = strlen(resname);\r
+\r
+ /* \r
+ * skip past the tads 2 file header (13 bytes for the signature, 7\r
+ * bytes for the version header, 2 bytes for the flags, 26 bytes for\r
+ * the timestamp) \r
+ */\r
+ p = basep + 13 + 7 + 2 + 26;\r
+\r
+ /* \r
+ * scan the sections in the file; stop on $EOF, and skip everything\r
+ * else but HTMLRES, which is the section type that \r
+ */\r
+ while (p < endp)\r
+ {\r
+ unsigned long endofs;\r
+\r
+ /*\r
+ * We're pointing to a section block header, which looks like this:\r
+ * \r
+ *. <byte> type-length\r
+ *. <byte * type-length> type-name\r
+ *. <uint32> next-section-address\r
+ */\r
+\r
+ /* read the ending offset */\r
+ endofs = osrp4(p + 1 + osrp1(p));\r
+\r
+ /* check the type */\r
+ if (p[0] == 7 && memcmp(p + 1, "HTMLRES", 7) == 0)\r
+ {\r
+ unsigned long found_ofs;\r
+ int found;\r
+ unsigned long entry_cnt;\r
+\r
+ /* we haven't found the resource yet */\r
+ found = FALSE;\r
+\r
+ /* \r
+ * It's a multimedia resource block. Skip the section block\r
+ * header and look at the index table - the index table\r
+ * consists of a uint32 giving the number of entries, followed\r
+ * by a reserved uint32, followed by the entries. \r
+ */\r
+ p += 12;\r
+ entry_cnt = osrp4(p);\r
+\r
+ /* skip to the first index entry */\r
+ p += 8;\r
+\r
+ /* scan the index entries */\r
+ for ( ; entry_cnt != 0 ; --entry_cnt)\r
+ {\r
+ unsigned long res_ofs;\r
+ unsigned long res_siz;\r
+ size_t name_len;\r
+\r
+ /*\r
+ * We're at the next index entry, which looks like this:\r
+ *\r
+ *. <uint32> resource-address (bytes from end of index)\r
+ *. <uint32> resource-length (in bytes)\r
+ *. <uint2> name-length\r
+ *. <byte * name-length> name\r
+ */\r
+ res_ofs = osrp4(p);\r
+ res_siz = osrp4(p + 4);\r
+ name_len = osrp2(p + 8);\r
+ p += 10;\r
+\r
+ /* check for a match to the name we're looking for */\r
+ if (name_len == resname_len\r
+ && my_memicmp(resname, p, name_len) == 0)\r
+ {\r
+ /* \r
+ * it's the one we want - note its resource location\r
+ * and size, but keep scanning for now, since we need\r
+ * to find the end of the index before we'll know where\r
+ * the actual resources begin \r
+ */\r
+ found = TRUE;\r
+ found_ofs = res_ofs;\r
+ info->len = res_siz;\r
+ }\r
+\r
+ /* skip this one's name */\r
+ p += name_len;\r
+ }\r
+\r
+ /* \r
+ * if we found our resource, the current seek position is the\r
+ * base of the offset we found in the directory; so we can\r
+ * finally fix up the offset to give the actual file location\r
+ * and return the result \r
+ */\r
+ if (found)\r
+ {\r
+ /* fix up the offset with the actual file location */\r
+ info->ptr = p + found_ofs;\r
+\r
+ /* tell the caller we found it */\r
+ return TRUE;\r
+ }\r
+ }\r
+ else if (p[0] == 4 && memcmp(p + 1, "$EOF", 4) == 0)\r
+ {\r
+ /* \r
+ * that's the end of the file - we've finished without finding\r
+ * the resource, so return failure \r
+ */\r
+ return FALSE;\r
+ }\r
+\r
+ /* move to the next section */\r
+ p = basep + endofs;\r
+ }\r
+\r
+ /* \r
+ * reached EOF without an $EOF marker - file must be corrupted; return\r
+ * 'not found' \r
+ */\r
+ return FALSE;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Find a resource in a T3 image file \r
+ */\r
+static int t3_find_res(const void *story_file, int32 story_len,\r
+ const char *resname, resinfo *info)\r
+{\r
+ const char *basep = (const char *)story_file;\r
+ const char *endp = basep + story_len;\r
+ const char *p;\r
+ size_t resname_len;\r
+\r
+ /* note the length of the name we're seeking */\r
+ resname_len = strlen(resname);\r
+\r
+ /* \r
+ * skip the file header - 11 bytes for the signature, 2 bytes for the\r
+ * format version, 32 reserved bytes, and 24 bytes for the timestamp \r
+ */\r
+ p = basep + 11 + 2 + 32 + 24;\r
+\r
+ /* scan the data blocks */\r
+ while (p < endp)\r
+ {\r
+ unsigned long siz;\r
+\r
+ /*\r
+ * We're at the next block header, which looks like this:\r
+ *\r
+ *. <byte * 4> type-name\r
+ *. <uint32> block-size\r
+ *. <uint16> flags\r
+ */\r
+\r
+ /* get the block size */\r
+ siz = osrp4(p + 4);\r
+\r
+ /* check the type */\r
+ if (memcmp(p, "MRES", 4) == 0)\r
+ {\r
+ unsigned int entry_cnt;\r
+ unsigned int i;\r
+ const char *blockp;\r
+\r
+ /* skip the header */\r
+ p += 10;\r
+\r
+ /* \r
+ * remember the location of the base of the block - the data\r
+ * seek location for each index entry is given as an offset\r
+ * from this location \r
+ */\r
+ blockp = p;\r
+\r
+ /* the first thing in the table is the number of entries */\r
+ entry_cnt = osrp2(p);\r
+ p += 2;\r
+\r
+ /* read the entries */\r
+ for (i = 0 ; i < entry_cnt ; ++i)\r
+ {\r
+ unsigned long entry_ofs;\r
+ unsigned long entry_siz;\r
+ size_t entry_name_len;\r
+ char namebuf[256];\r
+ char *xp;\r
+ size_t xi;\r
+\r
+ /* \r
+ * Parse this index entry:\r
+ * \r
+ *. <uint32> address (as offset from the block base)\r
+ *. <uint32> size (in bytes)\r
+ *. <uint8> name-length\r
+ *. <byte * name-length> name (all bytes XORed with 0xFF)\r
+ */\r
+ entry_ofs = osrp4(p);\r
+ entry_siz = osrp4(p + 4);\r
+ entry_name_len = (unsigned char)p[8];\r
+\r
+ /* unmask the name */\r
+ memcpy(namebuf, p + 9, resname_len);\r
+ for (xi = resname_len, xp = namebuf ; xi != 0 ; --xi)\r
+ *xp++ ^= 0xFF;\r
+\r
+ /* if this is the one we're looking for, return it */\r
+ if (entry_name_len == resname_len\r
+ && my_memicmp(resname, namebuf, resname_len) == 0)\r
+ {\r
+ /* \r
+ * fill in the return information - note that the entry\r
+ * offset given in the header is an offset from data\r
+ * block's starting location, so fix this up to an\r
+ * absolute seek location for the return value \r
+ */\r
+ info->ptr = blockp + entry_ofs;\r
+ info->len = entry_siz;\r
+\r
+ /* return success */\r
+ return TRUE;\r
+ }\r
+\r
+ /* skip this entry (header + name length) */\r
+ p += 9 + entry_name_len;\r
+ }\r
+\r
+ /* \r
+ * if we got this far, we didn't find the name; so skip past\r
+ * the MRES section by adding the section length to the base\r
+ * pointer, and resume the main file scan \r
+ */\r
+ p = blockp + siz;\r
+ }\r
+ else if (memcmp(p, "EOF ", 4) == 0)\r
+ {\r
+ /* \r
+ * end of file - we've finished without finding the resource,\r
+ * so return failure \r
+ */\r
+ return FALSE;\r
+ }\r
+ else\r
+ {\r
+ /* \r
+ * we don't care about anything else - just skip this block and\r
+ * keep going; to skip the block, simply seek ahead past the\r
+ * block header and then past the block's contents, using the\r
+ * size given the in block header \r
+ */\r
+ p += siz + 10;\r
+ }\r
+ }\r
+\r
+ /* \r
+ * reached EOF without an EOF marker - file must be corrupted; return\r
+ * 'not found' \r
+ */\r
+ return FALSE;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * JPEG and PNG information extraction (based on the versions in\r
+ * babel_story_functions.c) \r
+ */\r
+static int get_jpeg_dim(const void *img, int32 extent,\r
+ int32 *xout, int32 *yout)\r
+{\r
+ const unsigned char *dp=(const unsigned char *) img;\r
+ const unsigned char *ep=dp+extent;\r
+ unsigned int t1, t2, w, h;\r
+\r
+ t1 = *dp++;\r
+ t2 = *dp++;\r
+ if (t1 != 0xff || t2 != 0xD8)\r
+ return FALSE;\r
+\r
+ while(1)\r
+ {\r
+ if (dp>ep) return FALSE;\r
+ for(t1=*(dp++);t1!=0xff;t1=*(dp++)) if (dp>ep) return FALSE;\r
+ do { t1=*(dp++); if (dp>ep) return FALSE;} while (t1 == 0xff);\r
+\r
+ if ((t1 & 0xF0) == 0xC0 && !(t1==0xC4 || t1==0xC8 || t1==0xCC))\r
+ {\r
+ dp+=3;\r
+ if (dp>ep) return FALSE;\r
+ h=*(dp++) << 8;\r
+ if (dp>ep) return FALSE;\r
+ h|=*(dp++);\r
+ if (dp>ep) return FALSE;\r
+ w=*(dp++) << 8;\r
+ if (dp>ep) return FALSE;\r
+ w|=*(dp);\r
+\r
+ *xout = w;\r
+ *yout = h;\r
+ return TRUE;\r
+ }\r
+ else if (t1==0xD8 || t1==0xD9)\r
+ break;\r
+ else\r
+ {\r
+ int l;\r
+\r
+ if (dp>ep) return FALSE;\r
+ l=*(dp++) << 8;\r
+ if (dp>ep) return FALSE;\r
+ l|= *(dp++);\r
+ l-=2;\r
+ dp+=l;\r
+ if (dp>ep) return FALSE;\r
+ }\r
+ }\r
+ return FALSE;\r
+}\r
+\r
+static int32 png_read_int(const unsigned char *mem)\r
+{\r
+ int32 i4 = mem[0],\r
+ i3 = mem[1],\r
+ i2 = mem[2],\r
+ i1 = mem[3];\r
+ return i1 | (i2<<8) | (i3<<16) | (i4<<24);\r
+}\r
+\r
+\r
+static int get_png_dim(const void *img, int32 extent,\r
+ int32 *xout, int32 *yout)\r
+{\r
+ const unsigned char *dp=(const unsigned char *)img;\r
+\r
+ if (extent<33 ||\r
+ !(dp[0]==137 && dp[1]==80 && dp[2]==78 && dp[3]==71 &&\r
+ dp[4]==13 && dp[5] == 10 && dp[6] == 26 && dp[7]==10)||\r
+ !(dp[12]=='I' && dp[13]=='H' && dp[14]=='D' && dp[15]=='R'))\r
+ return FALSE;\r
+\r
+ *xout = png_read_int(dp+16);\r
+ *yout = png_read_int(dp+20);\r
+ return TRUE;\r
+}\r
+\r
+/* ------------------------------------------------------------------------ */\r
+/*\r
+ * Testing main() - this implements a set of unit tests on the tads\r
+ * version. \r
+ */\r
+\r
+#ifdef TADS_TEST\r
+\r
+#include "babel_handler.h"\r
+\r
+void main(int argc, char **argv)\r
+{\r
+ FILE *fp;\r
+ int32 siz;\r
+ void *buf;\r
+ valinfo *head;\r
+ int32 rv;\r
+ int tadsver;\r
+ char outbuf[TREATY_MINIMUM_EXTENT];\r
+\r
+ /* check arguments */\r
+ if (argc != 2)\r
+ {\r
+ printf("usage: tads <game-file>\n");\r
+ exit(1);\r
+ }\r
+\r
+ /* initialize the babel subsystems */\r
+ babel_init(argv[1]);\r
+\r
+ /* open the story file */\r
+ if ((fp = fopen(argv[1], "rb")) == 0)\r
+ {\r
+ printf("error opening input file\n");\r
+ exit(2);\r
+ }\r
+\r
+ /* check the file size */\r
+ fseek(fp, 0, SEEK_END);\r
+ siz = ftell(fp);\r
+ fseek(fp, 0, SEEK_SET);\r
+\r
+ /* allocate space for it */\r
+ if ((buf = malloc(siz)) == 0)\r
+ {\r
+ printf("error allocating space to load file\n");\r
+ exit(2);\r
+ }\r
+\r
+ /* load it */\r
+ if ((int32)fread(buf, 1, siz, fp) != siz)\r
+ {\r
+ printf("error reading file\n");\r
+ exit(2);\r
+ }\r
+\r
+ /* done with the file */\r
+ fclose(fp);\r
+\r
+\r
+\r
+ /* ===== test 1 - basic parse_game_info() test ===== */\r
+\r
+ /* parse the gameinfo record and print the results */\r
+ if ((head = parse_game_info(buf, siz, &tadsver)) != 0)\r
+ {\r
+ valinfo *val;\r
+\r
+ printf("found GameInfo - tads major version = %d\n", tadsver);\r
+ for (val = head ; val != 0 ; val = val->nxt)\r
+ {\r
+ printf("%.*s=[%.*s]\n",\r
+ (int)val->name_len, val->name,\r
+ (int)val->val_len, val->val);\r
+ }\r
+ printf("\n");\r
+ }\r
+ else\r
+ printf("no GameInfo found\n\n");\r
+\r
+\r
+\r
+ /* ===== test 2 - test the get_story_file_IFID generator ===== */\r
+ rv = tads_get_story_file_IFID(buf, siz, outbuf, TREATY_MINIMUM_EXTENT);\r
+ if (rv == 1)\r
+ printf("IFID = [%s]\n\n", outbuf);\r
+ else\r
+ printf("IFID return code = %ld\n", rv);\r
+\r
+\r
+\r
+ /* ===== test 3 - test the ifiction synthesizer ===== */\r
+ if ((rv = tads_get_story_file_metadata_extent(buf, siz)) > 0)\r
+ {\r
+ char *ifbuf;\r
+\r
+ /* try allocating the space */\r
+ if ((ifbuf = malloc((size_t)rv)) != 0)\r
+ {\r
+ /* synthesize the story file */\r
+ rv = tads_get_story_file_metadata(buf, siz, ifbuf, rv);\r
+ if (rv > 0)\r
+ printf("ifiction metadata:\n=====\n%.*s\n=====\n\n",\r
+ (int)rv, ifbuf);\r
+ else\r
+ printf("tads_get_story_file_metadata result = %ld\n", rv);\r
+ }\r
+ else\r
+ printf("unable to allocate %ld bytes for metadata record\n", rv);\r
+ }\r
+ else\r
+ printf("tads_get_story_file_metadata_extent result code = %ld\n", rv);\r
+ \r
+\r
+ /* free the loaded story file buffer */\r
+ free(buf);\r
+}\r
+\r
+\r
+#endif TADS_TEST\r
+\r