/*
     This file is part of libextractor.
     Copyright (C) 2026 Vidyut Samanta and Christian Grothoff

     libextractor is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published
     by the Free Software Foundation; either version 3, or (at your
     option) any later version.

     libextractor is distributed in the hope that it will be useful, but
     WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     General Public License for more details.

     You should have received a copy of the GNU General Public License
     along with libextractor; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.
 */
/**
 * @file plugins/rtf_extractor.c
 * @brief plugin to support Rich Text Format documents
 * @author Christian Grothoff
 *
 * RTF is the interchange format Microsoft Word has written since 1987
 * and, unlike the binary and OOXML formats, it is plain text -- which
 * is precisely why so much of it is still around, and why the meta
 * data it carries is so easy to overlook.
 *
 * Besides the document summary in the `\info' destination, an RTF file
 * routinely records:
 *
 * - `\author' and `\operator', that is who created the document and
 *   who last saved it.  These are frequently *different* people.
 * - `\*\company', the organisation the authoring copy of Word was
 *   licensed to.
 * - `\creatim', `\revtim' and `\printim', in *local* time and without
 *   a time zone, which leaks the author's time zone when compared to
 *   any absolute time stamp in the same document.
 * - `\*\revtbl', a table naming everybody who ever made a tracked
 *   change, plus `\revauth' / `\revdttm' (and the `\crauth', `\trauth'
 *   and `\pnrauth' variants) which index into it and carry the date
 *   and time of the individual edit.
 * - `\*\atnauthor' and `\*\atnid', the name and initials behind each
 *   comment.
 * - `\*\userprops', where Outlook leaves `_AuthorEmail' and
 *   `_AuthorEmailDisplayName' behind when a document is circulated for
 *   review.
 * - `\*\generator' and `\*\template', naming the exact application
 *   build and, not rarely, a local file system path.
 *
 * In a sample of the 1125 RTF files of the govdocs1 corpus, 60% carry
 * an `\author', 64% an `\operator', 53% a `\*\company' and 11% a
 * revision table.  See https://bugs.gnunet.org/view.php?id=2096.
 *
 * The parser is a streaming RTF tokenizer: it never holds more than a
 * fixed-size window of the file, skips the destinations that carry no
 * meta data (font tables, pictures, embedded objects and every unknown
 * ignorable destination), and bounds the number of groups, revision
 * table entries and reported values it is willing to track.  This
 * keeps it safe on hostile input, which matters because it runs on
 * whatever a privacy tool is asked to inspect.
 */
#include "platform.h"
#include <ctype.h>
#include "extractor.h"
#include "convert.h"


/**
 * Name this plugin reports itself as.
 */
#define PLUGIN_NAME "rtf"

/**
 * Longest meta data value we report.  Also bounds the buffer we
 * accumulate destination text in.
 */
#define MAX_VALUE 4096

/**
 * Deepest group nesting we track individually.  Anything below is
 * still counted, so that the matching closing braces are recognised,
 * but inherits the state of the deepest tracked group.
 */
#define MAX_DEPTH 128

/**
 * Longest control word we accept.  The specification limits control
 * words to 32 letters.
 */
#define MAX_CW 32

/**
 * Number of distinct strings we remember in order to suppress
 * duplicates.  Documents with tracked changes repeat the same handful
 * of names thousands of times.
 */
#define MAX_SEEN 256

/**
 * Number of entries we keep from the revision table.
 */
#define MAX_REVAUTHORS 64

/**
 * Number of distinct (author, date) pairs we keep from the revision
 * marks in the body.  They are resolved against the revision table
 * once the whole file has been read, as the table is allowed to appear
 * after the marks that use it.
 */
#define MAX_REVISIONS 64

/**
 * Maximum number of bytes we are willing to read from one file.
 */
#define MAX_SCAN_BYTES (64 * 1024 * 1024)

/**
 * Number of bytes we request from the datasource at a time.
 */
#define READ_CHUNK (64 * 1024)


/**
 * Destinations we distinguish.  Everything not listed here is either
 * transparent (text in it belongs to the enclosing destination) or
 * skipped wholesale.
 */
enum Destination
{
  /**
   * Nothing in particular; text is discarded.
   */
  DEST_NONE = 0,

  /**
   * Content of this group is of no interest at all.
   */
  DEST_SKIP,

  /**
   * The `\info' group; holds the document summary.
   */
  DEST_INFO,

  /**
   * The `\*\revtbl' group; each of its subgroups names one author.
   */
  DEST_REVTBL,

  /**
   * The `\*\userprops' group; holds custom document properties.
   */
  DEST_USERPROPS,

  /**
   * The `\*\annotation' group.  We have no use for the text of a
   * comment, but we do want to see the `\*\atndate' inside it, so it
   * must not be skipped like an unknown ignorable destination.
   */
  DEST_ANNOTATION,

  /* The remaining destinations hold text that we report.  They must
     stay contiguous and DEST_FIRST_TEXT/DEST_LAST_TEXT must bracket
     them; see is_text_dest(). */

  DEST_TITLE,
  DEST_SUBJECT,
  DEST_AUTHOR,
  DEST_OPERATOR,
  DEST_MANAGER,
  DEST_COMPANY,
  DEST_CATEGORY,
  DEST_KEYWORDS,
  DEST_DOCCOMM,
  DEST_HLINKBASE,
  DEST_GENERATOR,
  DEST_TEMPLATE,
  DEST_ATNAUTHOR,
  DEST_ATNID,
  DEST_PROPNAME,
  DEST_STATICVAL,
  DEST_ATNDATE,

  /* Date destinations; they hold `\yr', `\mo', ... rather than text. */

  DEST_CREATIM,
  DEST_REVTIM,
  DEST_PRINTIM,
  DEST_BUPTIM
};

/**
 * First destination that accumulates text.
 */
#define DEST_FIRST_TEXT DEST_TITLE

/**
 * Last destination that accumulates text.
 */
#define DEST_LAST_TEXT DEST_ATNDATE

/**
 * First destination that assembles a date.
 */
#define DEST_FIRST_DATE DEST_CREATIM


/**
 * State of one RTF group.  Pushed on `{', popped on `}'.
 */
struct Group
{
  /**
   * Destination in effect; inherited from the enclosing group unless
   * this group introduced one of its own.
   */
  enum Destination dest;

  /**
   * Number of bytes an unrepresentable Unicode character is replaced
   * by, from `\ucN'.  Scoped like a character property.
   */
  int uc;

  /**
   * True if this group introduced @e dest, and hence has to flush it
   * when it is closed.
   */
  int owns;

  /**
   * True if the content of this group is to be ignored entirely.
   */
  int skip;
};


/**
 * State kept while extracting from one document.
 */
struct RtfContext
{
  /**
   * Extraction context we were called with.
   */
  struct EXTRACTOR_ExtractContext *ec;

  /**
   * Set to 1 once the caller asked us to stop.
   */
  int stop;

  /**
   * Buffer most recently returned by the datasource.
   */
  const unsigned char *rbuf;

  /**
   * Number of valid bytes in @e rbuf.
   */
  size_t rhave;

  /**
   * Read position in @e rbuf.
   */
  size_t rpos;

  /**
   * Single byte pushed back, or -1.
   */
  int pushback;

  /**
   * Total number of bytes read so far.
   */
  uint64_t consumed;

  /**
   * Set once the datasource is exhausted.
   */
  int eof;

  /**
   * Group stack; entry 0 is the state outside of any group.
   */
  struct Group stack[MAX_DEPTH];

  /**
   * Index of the innermost tracked group in @e stack.
   */
  int depth;

  /**
   * Number of groups nested below @e stack[MAX_DEPTH - 1] that we
   * could not track individually.
   */
  unsigned int extra_depth;

  /**
   * Number of RTF `characters' still to be skipped because of a
   * preceding `\uN'.
   */
  int skip_units;

  /**
   * Set by `\*'; the control word that follows introduces an ignorable
   * destination.
   */
  int pending_ignorable;

  /**
   * UTF-8 text accumulated for the current destination.
   */
  char utf8[MAX_VALUE + 1];

  /**
   * Number of bytes used in @e utf8.
   */
  size_t utf8_len;

  /**
   * Bytes accumulated in @e charset, not yet converted.
   */
  char raw[MAX_VALUE + 1];

  /**
   * Number of bytes used in @e raw.
   */
  size_t raw_len;

  /**
   * Character set the `\'hh' escapes and plain bytes are in.
   */
  const char *charset;

  /**
   * True if @e charset was declared by the document rather than being
   * our fallback.
   */
  int charset_declared;

  /**
   * Platform the document was written on, if the character set
   * keyword gives it away, otherwise NULL.  `\ansi' does not: every
   * producer on every system writes that one.
   */
  const char *authoring_os;

  /**
   * High surrogate seen in a `\uN', waiting for its low half.
   */
  unsigned int surrogate;

  /**
   * Fields of the date currently being assembled, and whether we saw
   * a `\yr' at all.
   */
  int have_date;
  int d_yr;
  int d_mo;
  int d_dy;
  int d_hr;
  int d_mi;
  int d_se;

  /**
   * Name of the custom document property we are inside of.
   */
  char propname[256];

  /**
   * Names from the revision table, in table order.
   */
  char *revauth[MAX_REVAUTHORS];

  /**
   * Number of used entries in @e revauth.
   */
  unsigned int revauth_len;

  /**
   * Revision marks seen in the body, to be resolved against
   * @e revauth at the end.
   */
  struct
  {
    /**
     * Index into the revision table.
     */
    unsigned int idx;

    /**
     * Packed date and time of the edit.
     */
    uint32_t dttm;
  } rev[MAX_REVISIONS];

  /**
   * Number of used entries in @e rev.
   */
  unsigned int rev_len;

  /**
   * Author index from the most recent `\revauth' and friends, or -1.
   */
  int pending_author;

  /**
   * Author of the annotation we are in the middle of, if any.
   */
  char atn_author[256];

  /**
   * Strings we have already reported, to suppress duplicates.
   */
  char *seen[MAX_SEEN];

  /**
   * Number of used entries in @e seen.
   */
  unsigned int seen_len;
};


/**
 * Map from a control word introducing a destination to the
 * destination it introduces.
 */
static const struct
{
  const char *word;
  enum Destination dest;
} dest_words[] = {
  { "info",       DEST_INFO },
  { "revtbl",     DEST_REVTBL },
  { "userprops",  DEST_USERPROPS },
  { "annotation", DEST_ANNOTATION },
  { "title",      DEST_TITLE },
  { "subject",    DEST_SUBJECT },
  { "author",     DEST_AUTHOR },
  { "operator",   DEST_OPERATOR },
  { "manager",    DEST_MANAGER },
  { "company",    DEST_COMPANY },
  { "category",   DEST_CATEGORY },
  { "keywords",   DEST_KEYWORDS },
  { "doccomm",    DEST_DOCCOMM },
  { "hlinkbase",  DEST_HLINKBASE },
  { "generator",  DEST_GENERATOR },
  { "template",   DEST_TEMPLATE },
  { "atnauthor",  DEST_ATNAUTHOR },
  { "atnid",      DEST_ATNID },
  { "atndate",    DEST_ATNDATE },
  { "propname",   DEST_PROPNAME },
  { "staticval",  DEST_STATICVAL },
  { "creatim",    DEST_CREATIM },
  { "revtim",     DEST_REVTIM },
  { "printim",    DEST_PRINTIM },
  { "buptim",     DEST_BUPTIM },
  { NULL,         DEST_NONE }
};


/**
 * Destinations that are not marked ignorable but that we still have no
 * use for.  Skipping them is not merely an optimisation: `\pict' and
 * `\objdata' hold megabytes of hex-encoded binary.
 */
static const char *skip_words[] = {
  "fonttbl",
  "colortbl",
  "stylesheet",
  "listtable",
  "listoverridetable",
  "pict",
  "objdata",
  "objalias",
  "objsect",
  "objclass",
  "objname",
  NULL
};


/**
 * Map from a text destination to the meta data type to report it as.
 */
static const struct
{
  enum Destination dest;
  enum EXTRACTOR_MetaType type;
} text_map[] = {
  { DEST_TITLE,      EXTRACTOR_METATYPE_TITLE },
  { DEST_SUBJECT,    EXTRACTOR_METATYPE_SUBJECT },
  { DEST_AUTHOR,     EXTRACTOR_METATYPE_AUTHOR_NAME },
  { DEST_OPERATOR,   EXTRACTOR_METATYPE_LAST_SAVED_BY },
  { DEST_MANAGER,    EXTRACTOR_METATYPE_MANAGER },
  { DEST_COMPANY,    EXTRACTOR_METATYPE_COMPANY },
  { DEST_CATEGORY,   EXTRACTOR_METATYPE_SECTION },
  { DEST_KEYWORDS,   EXTRACTOR_METATYPE_KEYWORDS },
  { DEST_DOCCOMM,    EXTRACTOR_METATYPE_COMMENT },
  { DEST_HLINKBASE,  EXTRACTOR_METATYPE_URL },
  { DEST_GENERATOR,  EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE },
  { DEST_TEMPLATE,   EXTRACTOR_METATYPE_TEMPLATE },
  { DEST_ATNAUTHOR,  EXTRACTOR_METATYPE_CONTRIBUTOR_NAME },
  { DEST_ATNID,      EXTRACTOR_METATYPE_CONTRIBUTOR_NAME },
  { DEST_NONE,       EXTRACTOR_METATYPE_UNKNOWN }
};


/**
 * Map from a date destination to the meta data type to report it as.
 */
static const struct
{
  enum Destination dest;
  enum EXTRACTOR_MetaType type;
} date_map[] = {
  { DEST_CREATIM,  EXTRACTOR_METATYPE_CREATION_DATE },
  { DEST_REVTIM,   EXTRACTOR_METATYPE_MODIFICATION_DATE },
  { DEST_PRINTIM,  EXTRACTOR_METATYPE_LAST_PRINTED },
  { DEST_BUPTIM,   EXTRACTOR_METATYPE_UNKNOWN_DATE },
  { DEST_NONE,     EXTRACTOR_METATYPE_UNKNOWN }
};


/**
 * Custom document properties that carry personal information.  Word
 * writes these when a document is circulated for review from Outlook,
 * and they routinely outlive the review.
 */
static const struct
{
  const char *name;
  enum EXTRACTOR_MetaType type;
} custom_map[] = {
  { "_AuthorEmail",             EXTRACTOR_METATYPE_AUTHOR_EMAIL },
  { "_AuthorEmailDisplayName",  EXTRACTOR_METATYPE_AUTHOR_NAME },
  { "_EmailSubject",            EXTRACTOR_METATYPE_SUBJECT },
  { NULL,                       EXTRACTOR_METATYPE_UNKNOWN }
};


/* ******************** generic helpers ******************** */


/**
 * Check whether @a d is a destination whose text we accumulate.
 *
 * @param d destination to check
 * @return true if text in @a d is to be collected
 */
static int
is_text_dest (enum Destination d)
{
  return (DEST_FIRST_TEXT <= d) && (DEST_LAST_TEXT >= d);
}


/**
 * Trim leading and trailing white space in @a s, in place.
 *
 * @param s 0-terminated string to trim
 * @return pointer into @a s to the first non-blank character
 */
static char *
trim (char *s)
{
  size_t len = strlen (s);

  while ( (0 < len) &&
          (isspace ((unsigned char) s[len - 1])) )
    s[--len] = '\0';
  while (isspace ((unsigned char) s[0]))
    s++;
  return s;
}


/**
 * Check whether @a value was reported before, and remember it if not.
 *
 * @param rc our extraction state
 * @param value string to check
 * @return 1 if @a value is new (and was remembered), 0 if it is a
 *         duplicate or if we ran out of space to remember it
 */
static int
mark_seen (struct RtfContext *rc,
           const char *value)
{
  unsigned int i;
  char *dup;

  for (i = 0; i < rc->seen_len; i++)
    if (0 == strcmp (rc->seen[i], value))
      return 0;
  if (MAX_SEEN == rc->seen_len)
    return 0;
  if (NULL == (dup = strdup (value)))
    return 0;
  rc->seen[rc->seen_len++] = dup;
  return 1;
}


/**
 * Check that @a s is well-formed UTF-8.
 *
 * We announce our values as #EXTRACTOR_METAFORMAT_UTF8, so a string
 * the character set conversion could not handle has to be dropped
 * rather than passed on.  This happens for documents that use a code
 * page they never declare: `\ansicpg' is optional, and a file written
 * with a double byte character set but no `\ansicpg' cannot be decoded
 * from the `\info' group alone.
 *
 * @param s 0-terminated string to check
 * @return true if @a s is valid UTF-8
 */
static int
is_utf8 (const char *s)
{
  const unsigned char *p = (const unsigned char *) s;

  while ('\0' != p[0])
  {
    unsigned int extra;
    unsigned int i;
    uint32_t cp;

    if (0x80 > p[0])
    {
      p++;
      continue;
    }
    if ( (0xC2 <= p[0]) &&
         (0xDF >= p[0]) )
    {
      extra = 1;
      cp = p[0] & 0x1FU;
    }
    else if ( (0xE0 <= p[0]) &&
              (0xEF >= p[0]) )
    {
      extra = 2;
      cp = p[0] & 0x0FU;
    }
    else if ( (0xF0 <= p[0]) &&
              (0xF4 >= p[0]) )
    {
      extra = 3;
      cp = p[0] & 0x07U;
    }
    else
    {
      return 0;
    }
    for (i = 1; i <= extra; i++)
    {
      /* A premature 0-terminator fails this test as well, so we never
         read beyond the end of the string. */
      if (0x80 != (p[i] & 0xC0))
        return 0;
      cp = (cp << 6) | (p[i] & 0x3FU);
    }
    if ( ( (1 == extra) && (0x80 > cp) ) ||
         ( (2 == extra) && (0x800 > cp) ) ||
         ( (3 == extra) && (0x10000 > cp) ) ||
         (0x10FFFF < cp) ||
         ( (0xD800 <= cp) && (0xDFFF >= cp) ) )
      return 0;   /* overlong, out of range or a surrogate half */
    p += extra + 1;
  }
  return 1;
}


/**
 * Report a meta data value, unless it is empty or a duplicate.
 *
 * @param rc our extraction state
 * @param type meta data type to report the value as
 * @param value the value; leading and trailing white space is removed
 */
static void
add_meta (struct RtfContext *rc,
          enum EXTRACTOR_MetaType type,
          const char *value)
{
  char *tmp;
  char *val;

  if ( (0 != rc->stop) ||
       (NULL == value) )
    return;
  if (MAX_VALUE < strlen (value))
    return;
  if (NULL == (tmp = strdup (value)))
    return;
  val = trim (tmp);
  if ( ('\0' == val[0]) ||
       (0 == is_utf8 (val)) )
  {
    free (tmp);
    return;
  }
  /* De-duplicate per type: the same name legitimately shows up as both
     an author and a comment author, and both are worth reporting. */
  {
    char full[MAX_VALUE + 64];

    snprintf (full,
              sizeof (full),
              "%d:%s",
              (int) type,
              val);
    if (0 == mark_seen (rc, full))
    {
      free (tmp);
      return;
    }
  }
  if (0 != rc->ec->proc (rc->ec->cls,
                         PLUGIN_NAME,
                         type,
                         EXTRACTOR_METAFORMAT_UTF8,
                         "text/plain",
                         val,
                         strlen (val) + 1))
    rc->stop = 1;
  free (tmp);
}


/* ******************** reading ******************** */


/**
 * Obtain the next byte of the file.
 *
 * @param rc our extraction state
 * @return the byte, or -1 at the end of the file
 */
static int
rtf_getc (struct RtfContext *rc)
{
  void *data;
  ssize_t got;

  if (0 <= rc->pushback)
  {
    int c = rc->pushback;

    rc->pushback = -1;
    return c;
  }
  if (rc->rpos < rc->rhave)
    return rc->rbuf[rc->rpos++];
  if (0 != rc->eof)
    return -1;
  if (MAX_SCAN_BYTES <= rc->consumed)
  {
    rc->eof = 1;
    return -1;
  }
  got = rc->ec->read (rc->ec->cls,
                      &data,
                      READ_CHUNK);
  if ( (0 >= got) ||
       (NULL == data) )
  {
    rc->eof = 1;
    return -1;
  }
  rc->rbuf = data;
  rc->rhave = (size_t) got;
  rc->rpos = 0;
  rc->consumed += (uint64_t) got;
  return rc->rbuf[rc->rpos++];
}


/**
 * Push @a c back, to be returned by the next #rtf_getc().  At most one
 * byte can be pushed back at a time.
 *
 * @param rc our extraction state
 * @param c byte to push back, or -1 for none
 */
static void
rtf_ungetc (struct RtfContext *rc,
            int c)
{
  if (0 <= c)
    rc->pushback = c;
}


/* ******************** text accumulation ******************** */


/**
 * Convert the bytes accumulated in the current character set and
 * append the result to the UTF-8 buffer.
 *
 * @param rc our extraction state
 */
static void
flush_raw (struct RtfContext *rc)
{
  char *conv;
  size_t len;

  if (0 == rc->raw_len)
    return;
  conv = EXTRACTOR_common_convert_to_utf8 (rc->raw,
                                           rc->raw_len,
                                           rc->charset);
  rc->raw_len = 0;
  if (NULL == conv)
    return;
  len = strlen (conv);
  if (len > MAX_VALUE - rc->utf8_len)
    len = MAX_VALUE - rc->utf8_len;
  memcpy (&rc->utf8[rc->utf8_len],
          conv,
          len);
  rc->utf8_len += len;
  rc->utf8[rc->utf8_len] = '\0';
  free (conv);
}


/**
 * Append the byte @a b, which is in the document's character set, to
 * the text of the current destination.
 *
 * @param rc our extraction state
 * @param b byte to append
 */
static void
add_raw (struct RtfContext *rc,
         unsigned char b)
{
  if (MAX_VALUE <= rc->raw_len)
    flush_raw (rc);
  if (MAX_VALUE <= rc->raw_len)
    return;
  rc->raw[rc->raw_len++] = (char) b;
}


/**
 * Append the Unicode code point @a cp to the text of the current
 * destination.
 *
 * @param rc our extraction state
 * @param cp code point to append
 */
static void
add_codepoint (struct RtfContext *rc,
               unsigned int cp)
{
  char buf[4];
  size_t n;

  flush_raw (rc);
  if (0x80 > cp)
  {
    buf[0] = (char) cp;
    n = 1;
  }
  else if (0x800 > cp)
  {
    buf[0] = (char) (0xC0 | (cp >> 6));
    buf[1] = (char) (0x80 | (cp & 0x3F));
    n = 2;
  }
  else if (0x10000 > cp)
  {
    buf[0] = (char) (0xE0 | (cp >> 12));
    buf[1] = (char) (0x80 | ((cp >> 6) & 0x3F));
    buf[2] = (char) (0x80 | (cp & 0x3F));
    n = 3;
  }
  else
  {
    buf[0] = (char) (0xF0 | (cp >> 18));
    buf[1] = (char) (0x80 | ((cp >> 12) & 0x3F));
    buf[2] = (char) (0x80 | ((cp >> 6) & 0x3F));
    buf[3] = (char) (0x80 | (cp & 0x3F));
    n = 4;
  }
  if (n > MAX_VALUE - rc->utf8_len)
    return;
  memcpy (&rc->utf8[rc->utf8_len],
          buf,
          n);
  rc->utf8_len += n;
  rc->utf8[rc->utf8_len] = '\0';
}


/**
 * Discard whatever text was accumulated so far.
 *
 * @param rc our extraction state
 */
static void
reset_text (struct RtfContext *rc)
{
  rc->utf8_len = 0;
  rc->utf8[0] = '\0';
  rc->raw_len = 0;
  rc->surrogate = 0;
}


/**
 * Obtain the accumulated text of the current destination.
 *
 * @param rc our extraction state
 * @return 0-terminated UTF-8 string owned by @a rc
 */
static const char *
get_text (struct RtfContext *rc)
{
  flush_raw (rc);
  return rc->utf8;
}


/* ******************** dates ******************** */


/**
 * Format the date currently being assembled.
 *
 * RTF stores local time and gives no time zone, which is exactly what
 * makes these stamps interesting: compared against any absolute time
 * in the same document they reveal where the author sat.  We therefore
 * report them verbatim rather than pretending they were UTC.
 *
 * @param rc our extraction state
 * @param buf where to write the result
 * @param buf_size number of bytes available in @a buf
 * @return true if a plausible date was formatted
 */
static int
format_date (struct RtfContext *rc,
             char *buf,
             size_t buf_size)
{
  if ( (0 == rc->have_date) ||
       (1600 > rc->d_yr) ||
       (9999 < rc->d_yr) ||
       (1 > rc->d_mo) ||
       (12 < rc->d_mo) ||
       (1 > rc->d_dy) ||
       (31 < rc->d_dy) ||
       (0 > rc->d_hr) ||
       (23 < rc->d_hr) ||
       (0 > rc->d_mi) ||
       (59 < rc->d_mi) ||
       (0 > rc->d_se) ||
       (59 < rc->d_se) )
    return 0;
  return (0 < snprintf (buf,
                        buf_size,
                        "%04d-%02d-%02dT%02d:%02d:%02d",
                        rc->d_yr,
                        rc->d_mo,
                        rc->d_dy,
                        rc->d_hr,
                        rc->d_mi,
                        rc->d_se));
}


/**
 * Format the packed date and time @a dttm as used by the revision
 * marks.  Its layout is given in the RTF specification: minute in bits
 * 0-5, hour in 6-10, day of month in 11-15, month in 16-19, year minus
 * 1900 in 20-28 and the day of the week in 29-31.
 *
 * @param dttm packed value
 * @param buf where to write the result
 * @param buf_size number of bytes available in @a buf
 * @return true if a plausible date was formatted
 */
static int
format_dttm (uint32_t dttm,
             char *buf,
             size_t buf_size)
{
  unsigned int mi = dttm & 0x3F;
  unsigned int hr = (dttm >> 6) & 0x1F;
  unsigned int dy = (dttm >> 11) & 0x1F;
  unsigned int mo = (dttm >> 16) & 0x0F;
  unsigned int yr = 1900 + ((dttm >> 20) & 0x1FF);

  if ( (1 > mo) ||
       (12 < mo) ||
       (1 > dy) ||
       (31 < dy) ||
       (23 < hr) ||
       (59 < mi) ||
       (1980 > yr) )
    return 0;
  return (0 < snprintf (buf,
                        buf_size,
                        "%04u-%02u-%02uT%02u:%02u:00",
                        yr,
                        mo,
                        dy,
                        hr,
                        mi));
}


/* ******************** reporting ******************** */


/**
 * Report that @a author worked on the document at @a date.
 *
 * @param rc our extraction state
 * @param fmt format string taking the author and the date
 * @param author name of the person
 * @param date when they edited the document
 */
static void
add_revision (struct RtfContext *rc,
              const char *fmt,
              const char *author,
              const char *date)
{
  size_t bsize = strlen (author) + strlen (date) + strlen (fmt) + 1;
  char *line;

  if (MAX_VALUE < bsize)
    return;
  if (NULL == (line = malloc (bsize)))
    return;
  if (0 < snprintf (line,
                    bsize,
                    fmt,
                    author,
                    date))
    add_meta (rc,
              EXTRACTOR_METATYPE_REVISION_HISTORY,
              line);
  free (line);
}


/**
 * Remember an entry of the revision table and report the name.
 *
 * @param rc our extraction state
 * @param name name of the author, may be the placeholder `Unknown'
 */
static void
add_revauthor (struct RtfContext *rc,
               const char *name)
{
  char *dup;
  char *val;

  if (NULL == (dup = strdup (name)))
    return;
  val = trim (dup);
  /* Strip the terminating semicolon of the `{Author;}' subgroup. */
  {
    size_t len = strlen (val);

    if ( (0 < len) &&
         (';' == val[len - 1]) )
      val[len - 1] = '\0';
    val = trim (val);
  }
  if (MAX_REVAUTHORS > rc->revauth_len)
  {
    char *keep;

    if (NULL != (keep = strdup (val)))
      rc->revauth[rc->revauth_len++] = keep;
  }
  /* Entry 0 of the table is the placeholder for `no author'. */
  if (0 != strcmp (val, "Unknown"))
    add_meta (rc,
              EXTRACTOR_METATYPE_CONTRIBUTOR_NAME,
              val);
  free (dup);
}


/**
 * Remember a revision mark from the body, to be resolved against the
 * revision table once the whole file has been read.
 *
 * @param rc our extraction state
 * @param idx index into the revision table
 * @param dttm packed date and time of the edit
 */
static void
add_revision_mark (struct RtfContext *rc,
                   unsigned int idx,
                   uint32_t dttm)
{
  unsigned int i;

  for (i = 0; i < rc->rev_len; i++)
    if ( (rc->rev[i].idx == idx) &&
         (rc->rev[i].dttm == dttm) )
      return;
  if (MAX_REVISIONS == rc->rev_len)
    return;
  rc->rev[rc->rev_len].idx = idx;
  rc->rev[rc->rev_len].dttm = dttm;
  rc->rev_len++;
}


/**
 * Report that the author of the comment we are in the middle of wrote
 * it at the packed time @a dttm.
 *
 * @param rc our extraction state
 * @param dttm packed date and time
 */
static void
report_annotation_date (struct RtfContext *rc,
                        uint32_t dttm)
{
  char buf[32];

  if ('\0' == rc->atn_author[0])
    return;
  if (0 == format_dttm (dttm, buf, sizeof (buf)))
    return;
  add_revision (rc,
                _ ("Author `%s' commented on the document on `%s'"),
                rc->atn_author,
                buf);
}


/**
 * Report the value of the custom document property @a rc->propname.
 *
 * @param rc our extraction state
 * @param value value of the property
 */
static void
report_property (struct RtfContext *rc,
                 const char *value)
{
  unsigned int i;

  if ('\0' == rc->propname[0])
    return;
  for (i = 0; NULL != custom_map[i].name; i++)
    if (0 == strcmp (custom_map[i].name, rc->propname))
    {
      add_meta (rc, custom_map[i].type, value);
      rc->propname[0] = '\0';
      return;
    }
  /* Properties we have no type for are still worth reporting: this is
     where users park whatever their organisation asked them to. */
  if ('_' != rc->propname[0])
  {
    char line[MAX_VALUE + 1];

    if ( (0 < snprintf (line,
                        sizeof (line),
                        "%s: %s",
                        rc->propname,
                        value)) )
      add_meta (rc, EXTRACTOR_METATYPE_UNKNOWN, line);
  }
  rc->propname[0] = '\0';
}


/* ******************** group handling ******************** */


/**
 * Handle the end of the destination @a d.
 *
 * @param rc our extraction state
 * @param d destination that is being left
 */
static void
close_destination (struct RtfContext *rc,
                   enum Destination d)
{
  unsigned int i;

  if (is_text_dest (d))
  {
    const char *text = get_text (rc);

    switch (d)
    {
    case DEST_PROPNAME:
      {
        char *dup = strdup (text);

        rc->propname[0] = '\0';
        if (NULL != dup)
        {
          char *val = trim (dup);

          if (sizeof (rc->propname) > strlen (val))
            memcpy (rc->propname, val, strlen (val) + 1);
          free (dup);
        }
        break;
      }
    case DEST_STATICVAL:
      report_property (rc, text);
      break;
    case DEST_ATNDATE:
      {
        /* Some producers write the time stamp of a comment as the text
           of a `{\*\atndate ...}' destination rather than as the
           parameter of the control word. */
        char *endp;
        long long v = strtoll (text, &endp, 10);

        if ( (endp != text) &&
             (0 <= v) &&
             (0xFFFFFFFFLL >= v) )
          report_annotation_date (rc, (uint32_t) v);
        break;
      }
    case DEST_ATNAUTHOR:
    case DEST_ATNID:
      {
        char *dup = strdup (text);

        if (NULL != dup)
        {
          char *val = trim (dup);

          if ( ('\0' != val[0]) &&
               (sizeof (rc->atn_author) > strlen (val)) )
            memcpy (rc->atn_author, val, strlen (val) + 1);
          free (dup);
        }
        add_meta (rc, EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, text);
        break;
      }
    case DEST_GENERATOR:
      {
        /* Word terminates the generator string with a semicolon, the
           way it does the entries of the font and colour tables. */
        char *dup = strdup (text);

        if (NULL != dup)
        {
          size_t len = strlen (dup);

          if ( (0 < len) &&
               (';' == dup[len - 1]) )
            dup[len - 1] = '\0';
          add_meta (rc, EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, dup);
          free (dup);
        }
        break;
      }
    default:
      for (i = 0; DEST_NONE != text_map[i].dest; i++)
        if (text_map[i].dest == d)
        {
          add_meta (rc, text_map[i].type, text);
          break;
        }
      break;
    }
    reset_text (rc);
    return;
  }
  if (DEST_FIRST_DATE <= d)
  {
    char buf[32];

    if (0 != format_date (rc, buf, sizeof (buf)))
      for (i = 0; DEST_NONE != date_map[i].dest; i++)
        if (date_map[i].dest == d)
        {
          add_meta (rc, date_map[i].type, buf);
          break;
        }
    rc->have_date = 0;
    return;
  }
}


/**
 * Enter a new group.
 *
 * @param rc our extraction state
 */
static void
push_group (struct RtfContext *rc)
{
  rc->skip_units = 0;
  if (MAX_DEPTH - 1 > rc->depth)
  {
    rc->depth++;
    rc->stack[rc->depth] = rc->stack[rc->depth - 1];
    rc->stack[rc->depth].owns = 0;
    /* Each subgroup of the revision table names one author. */
    if (DEST_REVTBL == rc->stack[rc->depth].dest)
      reset_text (rc);
  }
  else
  {
    rc->extra_depth++;
  }
}


/**
 * Leave the innermost group.
 *
 * @param rc our extraction state
 */
static void
pop_group (struct RtfContext *rc)
{
  struct Group *g;

  rc->skip_units = 0;
  if (0 < rc->extra_depth)
  {
    rc->extra_depth--;
    return;
  }
  if (0 == rc->depth)
    return;
  g = &rc->stack[rc->depth];
  if (0 == g->skip)
  {
    if (0 != g->owns)
      close_destination (rc, g->dest);
    else if (DEST_REVTBL == g->dest)
      add_revauthor (rc, get_text (rc));
  }
  rc->depth--;
  if (DEST_REVTBL == rc->stack[rc->depth].dest)
    reset_text (rc);
}


/* ******************** control words ******************** */


/**
 * Note that the document is written in the code page @a cpg.
 *
 * @param cpg code page number from `\ansicpg'
 * @return name of the character set, NULL if unknown
 */
static const char *
codepage_to_charset (int64_t cpg)
{
  switch (cpg)
  {
  case 437:
    return "CP437";
  case 708:
    return "ISO-8859-6";
  case 850:
    return "CP850";
  case 852:
    return "CP852";
  case 862:
    return "CP862";
  case 864:
    return "CP864";
  case 866:
    return "CP866";
  case 874:
    return "CP874";
  case 932:
    return "CP932";
  case 936:
    return "CP936";
  case 949:
    return "CP949";
  case 950:
    return "CP950";
  case 1250:
    return "CP1250";
  case 1251:
    return "CP1251";
  case 1252:
    return "CP1252";
  case 1253:
    return "CP1253";
  case 1254:
    return "CP1254";
  case 1255:
    return "CP1255";
  case 1256:
    return "CP1256";
  case 1257:
    return "CP1257";
  case 1258:
    return "CP1258";
  case 10000:
    return "MACINTOSH";
  case 65001:
    return "UTF-8";
  default:
    return NULL;
  }
}


/**
 * Handle a control word that carries a component of the date currently
 * being assembled.
 *
 * @param rc our extraction state
 * @param word the control word, without the backslash
 * @param param its numeric parameter
 * @return true if @a word was a date component
 */
static int
handle_date_word (struct RtfContext *rc,
                  const char *word,
                  int64_t param)
{
  if (0 == strcmp (word, "yr"))
    rc->d_yr = (int) param;
  else if (0 == strcmp (word, "mo"))
    rc->d_mo = (int) param;
  else if (0 == strcmp (word, "dy"))
    rc->d_dy = (int) param;
  else if (0 == strcmp (word, "hr"))
    rc->d_hr = (int) param;
  else if (0 == strcmp (word, "min"))
    rc->d_mi = (int) param;
  else if (0 == strcmp (word, "sec"))
    rc->d_se = (int) param;
  else
    return 0;
  rc->have_date = 1;
  return 1;
}


/**
 * Handle a control word that reports a number from the `\info' group.
 *
 * @param rc our extraction state
 * @param word the control word, without the backslash
 * @param param its numeric parameter
 * @return true if @a word was such a control word
 */
static int
handle_info_number (struct RtfContext *rc,
                    const char *word,
                    int64_t param)
{
  static const struct
  {
    const char *word;
    enum EXTRACTOR_MetaType type;
  } map[] = {
    { "version",   EXTRACTOR_METATYPE_REVISION_NUMBER },
    { "edmins",    EXTRACTOR_METATYPE_TOTAL_EDITING_TIME },
    { "nofpages",  EXTRACTOR_METATYPE_PAGE_COUNT },
    { "nofwords",  EXTRACTOR_METATYPE_WORD_COUNT },
    { "nofchars",  EXTRACTOR_METATYPE_CHARACTER_COUNT },
    { NULL,        EXTRACTOR_METATYPE_UNKNOWN }
  };
  unsigned int i;

  for (i = 0; NULL != map[i].word; i++)
    if (0 == strcmp (map[i].word, word))
    {
      char buf[32];

      if (0 > param)
        return 1;
      snprintf (buf, sizeof (buf), "%llu", (unsigned long long) param);
      add_meta (rc, map[i].type, buf);
      return 1;
    }
  return 0;
}


/**
 * Handle a control word that identifies the author of a revision, or
 * the date of one.
 *
 * @param rc our extraction state
 * @param word the control word, without the backslash
 * @param param its numeric parameter
 * @return true if @a word was a revision mark
 */
static int
handle_revision_word (struct RtfContext *rc,
                      const char *word,
                      int64_t param)
{
  static const char *author_words[] = {
    "revauth",
    "revauthdel",
    "crauth",
    "trauth",
    "pnrauth",
    NULL
  };
  static const char *date_words[] = {
    "revdttm",
    "revdttmdel",
    "crdate",
    "trdate",
    "pnrdate",
    NULL
  };
  unsigned int i;

  for (i = 0; NULL != author_words[i]; i++)
    if (0 == strcmp (author_words[i], word))
    {
      rc->pending_author = ( (0 <= param) &&
                             (MAX_REVAUTHORS > param) )
                           ? (int) param
                           : -1;
      return 1;
    }
  for (i = 0; NULL != date_words[i]; i++)
    if (0 == strcmp (date_words[i], word))
    {
      if (0 <= rc->pending_author)
        add_revision_mark (rc,
                           (unsigned int) rc->pending_author,
                           (uint32_t) param);
      rc->pending_author = -1;
      return 1;
    }
  return 0;
}


/**
 * Act on the control word @a word.
 *
 * @param rc our extraction state
 * @param word the control word, without the backslash
 * @param has_param true if @a word had a numeric parameter
 * @param param the numeric parameter, 0 if there was none
 */
static void
handle_word (struct RtfContext *rc,
             const char *word,
             int has_param,
             int64_t param)
{
  struct Group *g = &rc->stack[rc->depth];
  int ignorable = rc->pending_ignorable;
  unsigned int i;

  rc->pending_ignorable = 0;
  if ( (0 != has_param) &&
       (0 == strcmp (word, "atndate")) )
  {
    report_annotation_date (rc, (uint32_t) param);
    return;
  }
  for (i = 0; NULL != dest_words[i].word; i++)
    if (0 == strcmp (dest_words[i].word, word))
    {
      g->dest = dest_words[i].dest;
      g->owns = 1;
      if (is_text_dest (g->dest))
        reset_text (rc);
      if (DEST_FIRST_DATE <= g->dest)
      {
        rc->have_date = 0;
        rc->d_yr = 0;
        rc->d_mo = 0;
        rc->d_dy = 0;
        rc->d_hr = 0;
        rc->d_mi = 0;
        rc->d_se = 0;
      }
      return;
    }
  if (0 != ignorable)
  {
    /* An ignorable destination we do not know: the specification says
       to discard it, which is what keeps us out of `\*\datastore',
       `\*\latentstyles', `\*\shppict' and the like. */
    g->dest = DEST_SKIP;
    g->owns = 0;
    g->skip = 1;
    return;
  }
  for (i = 0; NULL != skip_words[i]; i++)
    if (0 == strcmp (skip_words[i], word))
    {
      g->dest = DEST_SKIP;
      g->owns = 0;
      g->skip = 1;
      return;
    }
  if (0 == strcmp (word, "uc"))
  {
    if ( (0 != has_param) &&
         (0 <= param) &&
         (MAX_VALUE > param) )
      g->uc = (int) param;
    return;
  }
  if (0 == strcmp (word, "ansicpg"))
  {
    const char *cs = codepage_to_charset (param);

    if (NULL != cs)
    {
      rc->charset = cs;
      rc->charset_declared = 1;
    }
    return;
  }
  if (0 == strcmp (word, "mac"))
  {
    rc->charset = "MACINTOSH";
    rc->charset_declared = 1;
    rc->authoring_os = "Macintosh";
    return;
  }
  if (0 == strcmp (word, "pc"))
  {
    rc->charset = "CP437";
    rc->charset_declared = 1;
    rc->authoring_os = "MS-DOS";
    return;
  }
  if (0 == strcmp (word, "pca"))
  {
    rc->charset = "CP850";
    rc->charset_declared = 1;
    rc->authoring_os = "MS-DOS";
    return;
  }
  if (DEST_FIRST_DATE <= g->dest)
  {
    if (0 != handle_date_word (rc, word, param))
      return;
  }
  if (DEST_INFO == g->dest)
  {
    if (0 != handle_info_number (rc, word, param))
      return;
  }
  (void) handle_revision_word (rc, word, param);
}


/**
 * Act on a `\uN' Unicode escape.
 *
 * @param rc our extraction state
 * @param param the code point, as a signed 16 bit number
 */
static void
handle_unicode (struct RtfContext *rc,
                int64_t param)
{
  unsigned int cp;

  if (0 > param)
    param += 0x10000;
  if ( (0 > param) ||
       (0x10FFFF < param) )
  {
    rc->surrogate = 0;
    return;
  }
  cp = (unsigned int) param;
  if ( (0xD800 <= cp) &&
       (0xDBFF >= cp) )
  {
    /* Word splits characters beyond the basic multilingual plane into
       a surrogate pair of two `\uN'; hold the first half. */
    rc->surrogate = cp;
    return;
  }
  if ( (0xDC00 <= cp) &&
       (0xDFFF >= cp) )
  {
    if (0 != rc->surrogate)
    {
      unsigned int full = 0x10000
                          + ((rc->surrogate - 0xD800) << 10)
                          + (cp - 0xDC00);

      rc->surrogate = 0;
      add_codepoint (rc, full);
    }
    /* An unpaired surrogate has no UTF-8 encoding; drop it. */
    return;
  }
  rc->surrogate = 0;
  add_codepoint (rc, cp);
}


/**
 * Parse the control word or control symbol that follows a backslash.
 *
 * @param rc our extraction state
 * @param[out] word set to the control word, or to the single character
 *             of a control symbol
 * @param[out] has_param set to true if a numeric parameter followed
 * @param[out] param set to the numeric parameter
 * @return 1 for a control word, 0 for a control symbol, -1 at the end
 *         of the file
 */
static int
parse_control (struct RtfContext *rc,
               char *word,
               int *has_param,
               int64_t *param)
{
  int c = rtf_getc (rc);
  size_t n = 0;

  word[0] = '\0';
  *has_param = 0;
  *param = 0;
  if (0 > c)
    return -1;
  if (0 == isalpha ((unsigned char) c))
  {
    word[0] = (char) c;
    word[1] = '\0';
    return 0;
  }
  while ( (0 <= c) &&
          (0 != isalpha ((unsigned char) c)) )
  {
    if (MAX_CW > n)
      word[n++] = (char) c;
    c = rtf_getc (rc);
  }
  word[n] = '\0';
  if ( (0 <= c) &&
       ( ('-' == c) ||
         (0 != isdigit ((unsigned char) c)) ) )
  {
    int neg = ('-' == c);
    int64_t v = 0;
    unsigned int digits = 0;

    if (0 != neg)
      c = rtf_getc (rc);
    while ( (0 <= c) &&
            (0 != isdigit ((unsigned char) c)) )
    {
      if (10 > digits)
      {
        v = v * 10 + (c - '0');
        digits++;
      }
      c = rtf_getc (rc);
    }
    if (0 < digits)
    {
      *has_param = 1;
      *param = (0 != neg) ? -v : v;
    }
  }
  /* A single space after a control word is its delimiter and is not
     part of the document text. */
  if (' ' != c)
    rtf_ungetc (rc, c);
  return 1;
}


/**
 * Read and discard the next @a n bytes; used for the binary data that
 * follows `\binN'.
 *
 * @param rc our extraction state
 * @param n number of bytes to discard
 */
static void
skip_bytes (struct RtfContext *rc,
            int64_t n)
{
  while (0 < n)
  {
    if (0 > rtf_getc (rc))
      return;
    n--;
  }
}


/**
 * Act on a control symbol, that is a backslash followed by a character
 * that is not a letter.
 *
 * @param rc our extraction state
 * @param sym the character
 * @param collect true if we are inside a destination whose text we
 *        accumulate
 */
static void
handle_symbol (struct RtfContext *rc,
               char sym,
               int collect)
{
  switch (sym)
  {
  case '*':
    rc->pending_ignorable = 1;
    break;
  case '\\':
  case '{':
  case '}':
    if (0 != collect)
      add_raw (rc, (unsigned char) sym);
    break;
  case '\'':
    {
      int hi = rtf_getc (rc);
      int lo;

      if ( (0 > hi) ||
           (0 == isxdigit ((unsigned char) hi)) )
      {
        rtf_ungetc (rc, hi);
        break;
      }
      lo = rtf_getc (rc);
      if ( (0 > lo) ||
           (0 == isxdigit ((unsigned char) lo)) )
      {
        rtf_ungetc (rc, lo);
        break;
      }
      if (0 != collect)
      {
        int v = 0;

        v = (0 != isdigit ((unsigned char) hi))
            ? (hi - '0')
            : (tolower ((unsigned char) hi) - 'a' + 10);
        v <<= 4;
        v |= (0 != isdigit ((unsigned char) lo))
             ? (lo - '0')
             : (tolower ((unsigned char) lo) - 'a' + 10);
        add_raw (rc, (unsigned char) v);
      }
      break;
    }
  case '~':
    if (0 != collect)
      add_codepoint (rc, 0x00A0);
    break;
  case '_':
    if (0 != collect)
      add_raw (rc, (unsigned char) '-');
    break;
  case '\r':
  case '\n':
    if (0 != collect)
      add_raw (rc, (unsigned char) ' ');
    break;
  default:
    /* `\-' (optional hyphen), `\:' (subentry) and anything else we do
       not know produce no text. */
    break;
  }
}


/* ******************** main loop ******************** */


/**
 * Report everything we learned from the revision marks, now that the
 * revision table is complete.
 *
 * @param rc our extraction state
 */
static void
report_revisions (struct RtfContext *rc)
{
  unsigned int i;

  for (i = 0; i < rc->rev_len; i++)
  {
    char buf[32];
    const char *name;

    if (rc->rev[i].idx >= rc->revauth_len)
      continue;
    name = rc->revauth[rc->rev[i].idx];
    if ( (NULL == name) ||
         ('\0' == name[0]) ||
         (0 == strcmp (name, "Unknown")) )
      continue;
    if (0 == format_dttm (rc->rev[i].dttm, buf, sizeof (buf)))
      continue;
    add_revision (rc,
                  _ ("Author `%s' edited the document on `%s'"),
                  name,
                  buf);
  }
}


/**
 * Release everything @a rc holds on to.
 *
 * @param rc our extraction state
 */
static void
cleanup (struct RtfContext *rc)
{
  unsigned int i;

  for (i = 0; i < rc->seen_len; i++)
    free (rc->seen[i]);
  for (i = 0; i < rc->revauth_len; i++)
    free (rc->revauth[i]);
}


/**
 * Main entry method for the `rtf' extraction plugin.
 *
 * @param ec extraction context provided to the plugin
 */
void
EXTRACTOR_rtf_extract_method (struct EXTRACTOR_ExtractContext *ec);

void
EXTRACTOR_rtf_extract_method (struct EXTRACTOR_ExtractContext *ec)
{
  struct RtfContext *rc;
  void *data;

  if (5 > ec->read (ec->cls, &data, 5))
    return;
  if (0 != memcmp (data, "{\\rtf", 5))
    return;
  if (0 != ec->seek (ec->cls, 0, SEEK_SET))
    return;
  if (NULL == (rc = malloc (sizeof (struct RtfContext))))
    return;
  memset (rc, 0, sizeof (struct RtfContext));
  rc->ec = ec;
  rc->pushback = -1;
  rc->charset = "CP1252";
  rc->pending_author = -1;
  rc->stack[0].uc = 1;
  if (0 != ec->proc (ec->cls,
                     PLUGIN_NAME,
                     EXTRACTOR_METATYPE_MIMETYPE,
                     EXTRACTOR_METAFORMAT_UTF8,
                     "text/plain",
                     "text/rtf",
                     strlen ("text/rtf") + 1))
  {
    free (rc);
    return;
  }
  while (0 == rc->stop)
  {
    int c = rtf_getc (rc);
    int collect;

    if (0 > c)
      break;
    if ( ('{' == c) ||
         ('}' == c) )
    {
      /* A group boundary always ends the run of characters skipped
         because of a `\uN'. */
      if ('{' == c)
        push_group (rc);
      else
        pop_group (rc);
      continue;
    }
    collect = ( (0 == rc->stack[rc->depth].skip) &&
                ( (is_text_dest (rc->stack[rc->depth].dest)) ||
                  (DEST_REVTBL == rc->stack[rc->depth].dest) ) );
    if ('\\' != c)
    {
      if ( ('\r' == c) ||
           ('\n' == c) )
        continue;   /* not part of the document text */
      if (0 < rc->skip_units)
      {
        rc->skip_units--;
        continue;
      }
      if (0 != collect)
        add_raw (rc, (unsigned char) c);
      continue;
    }
    {
      char word[MAX_CW + 1];
      int has_param;
      int64_t param;
      int kind = parse_control (rc, word, &has_param, &param);

      if (0 > kind)
        break;
      if (0 < rc->skip_units)
      {
        /* A control word or symbol counts as one of the characters
           that `\uN' asked us to skip; a `\'hh' takes its two hex
           digits with it. */
        rc->skip_units--;
        if ( (0 == kind) &&
             ('\'' == word[0]) )
        {
          (void) rtf_getc (rc);
          (void) rtf_getc (rc);
        }
        continue;
      }
      if (0 == kind)
      {
        handle_symbol (rc, word[0], collect);
        continue;
      }
      if (0 == strcmp (word, "bin"))
      {
        if ( (0 != has_param) &&
             (0 < param) )
          skip_bytes (rc, param);
        continue;
      }
      if (0 == strcmp (word, "u"))
      {
        if (0 != has_param)
        {
          if (0 != collect)
            handle_unicode (rc, param);
          rc->skip_units = rc->stack[rc->depth].uc;
        }
        continue;
      }
      if (0 != rc->stack[rc->depth].skip)
      {
        /* Still track `\*' so that a nested unknown destination inside
           a skipped one does not confuse us. */
        rc->pending_ignorable = 0;
        continue;
      }
      handle_word (rc, word, has_param, param);
    }
  }
  if (0 != rc->charset_declared)
    add_meta (rc, EXTRACTOR_METATYPE_CHARACTER_SET, rc->charset);
  if (NULL != rc->authoring_os)
    add_meta (rc, EXTRACTOR_METATYPE_AUTHORING_OS, rc->authoring_os);
  report_revisions (rc);
  cleanup (rc);
  free (rc);
}


/* end of rtf_extractor.c */
