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