Merge branch 'master' into browser
[projects/chimara/chimara.git] / babel / tads.c
diff --git a/babel/tads.c b/babel/tads.c
new file mode 100644 (file)
index 0000000..bde1e5a
--- /dev/null
@@ -0,0 +1,1827 @@
+/* \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 '&lt;', '&gt;', and '&amp;',\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, "&lt;");\r
+            ++p, --len;\r
+            break;\r
+\r
+        case '>':\r
+            write_ifiction_z(ctx, "&gt;");\r
+            ++p, --len;\r
+            break;\r
+\r
+        case '&':\r
+            write_ifiction_z(ctx, "&amp;");\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