/*
     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/msoffice_extractor.c
 * @brief plugin to support Microsoft Office documents that are not
 *        stored in an OLE2 container
 * @author Christian Grothoff
 *
 * The `ole2' plugin covers the OLE2-based generation of Microsoft
 * Office formats (.doc, .xls, .ppt) and needs libgsf to open the
 * container.  This plugin covers the two families that live outside of
 * an OLE2 container and hence need no libgsf at all:
 *
 * - Office Open XML (.docx, .xlsx, .pptx and their macro-enabled and
 *   template variants), which is a ZIP archive of XML parts.
 * - Bare BIFF record streams, which is how Excel 2 to Excel 4 wrote
 *   .xls files before OLE2 existed.  These are still common.
 *
 * Beyond the document summary, both carry a considerable amount of
 * information about *who* edited a document and *when*: tracked
 * changes, comment authors, the identities used to author them,
 * shared-workbook revision logs, the "send for review" e-mail
 * addresses left behind by Outlook, and the user name recorded in the
 * Excel file protection records.  Extracting this is the point of
 * https://bugs.gnunet.org/view.php?id=2096 -- it matters both for
 * forensics and for privacy tools that want to warn about the
 * information a document leaks.
 */
#include "platform.h"
#include <ctype.h>
#include "extractor.h"
#include "convert.h"
#include "unzip.h"
#include "msoffice_biff.h"


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

/**
 * Maximum length of a file name inside the ZIP archive.
 */
#define MAXFILENAME 256

/**
 * Maximum number of interesting ZIP entries we remember.  Documents
 * with more comment or revision parts than this are pathological.
 */
#define MAX_PARTS 64

/**
 * Maximum size of a part that we read into memory as a whole.  Only
 * used for the small property parts.
 */
#define MAX_SMALL_PART (256 * 1024)

/**
 * Size of the sliding window used to scan the large parts.
 */
#define SCAN_CHUNK (128 * 1024)

/**
 * Longest XML tag we are able to look at in one piece.  Tags carrying
 * an author are far shorter; anything longer is skipped rather than
 * buffered, which bounds our memory use on hostile input.
 */
#define MAX_TAG 8192

/**
 * Maximum number of bytes we scan in a single part.  Guards against
 * decompression bombs.
 */
#define MAX_SCAN_BYTES (64 * 1024 * 1024)

/**
 * Maximum number of distinct strings we remember to suppress
 * duplicates.  A document with tracked changes typically repeats the
 * same handful of authors thousands of times.
 */
#define MAX_SEEN 256

/**
 * Longest metadata value we report.
 */
#define MAX_VALUE 4096


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

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

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

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


/**
 * Entry in a map from XML element names to LE meta data types.
 */
struct Matches
{
  /**
   * Name of the XML element.
   */
  const char *text;

  /**
   * Corresponding LE type.
   */
  enum EXTRACTOR_MetaType type;
};


/**
 * Elements of the OPC core properties part (`docProps/core.xml'),
 * which is the Dublin Core derived summary of the document.
 */
static struct Matches core_map[] = {
  { "dc:title",           EXTRACTOR_METATYPE_TITLE },
  { "dc:subject",         EXTRACTOR_METATYPE_SUBJECT },
  { "dc:creator",         EXTRACTOR_METATYPE_CREATOR },
  { "dc:description",     EXTRACTOR_METATYPE_DESCRIPTION },
  { "dc:language",        EXTRACTOR_METATYPE_LANGUAGE },
  { "cp:keywords",        EXTRACTOR_METATYPE_KEYWORDS },
  { "cp:category",        EXTRACTOR_METATYPE_SECTION },
  { "cp:lastModifiedBy",  EXTRACTOR_METATYPE_LAST_SAVED_BY },
  { "cp:revision",        EXTRACTOR_METATYPE_REVISION_NUMBER },
  { "cp:lastPrinted",     EXTRACTOR_METATYPE_LAST_PRINTED },
  { "dcterms:created",    EXTRACTOR_METATYPE_CREATION_DATE },
  { "dcterms:modified",   EXTRACTOR_METATYPE_MODIFICATION_DATE },
  { NULL, 0 }
};


/**
 * Elements of the extended properties part (`docProps/app.xml'),
 * which is written by the application that saved the document.
 */
static struct Matches app_map[] = {
  { "Application",    EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE },
  { "AppVersion",     EXTRACTOR_METATYPE_SOFTWARE_VERSION },
  { "Company",        EXTRACTOR_METATYPE_COMPANY },
  { "Manager",        EXTRACTOR_METATYPE_MANAGER },
  { "Template",       EXTRACTOR_METATYPE_TEMPLATE },
  { "TotalTime",      EXTRACTOR_METATYPE_TOTAL_EDITING_TIME },
  { "Pages",          EXTRACTOR_METATYPE_PAGE_COUNT },
  { "Words",          EXTRACTOR_METATYPE_WORD_COUNT },
  { "Characters",     EXTRACTOR_METATYPE_CHARACTER_COUNT },
  { "Lines",          EXTRACTOR_METATYPE_LINE_COUNT },
  { "Paragraphs",     EXTRACTOR_METATYPE_PARAGRAPH_COUNT },
  { NULL, 0 }
};


/**
 * Custom properties (`docProps/custom.xml') that carry personal
 * information.  Word writes these when a document is sent around for
 * review from Outlook; they routinely outlive the review and identify
 * the person who circulated the document.
 */
static struct Matches custom_map[] = {
  { "_AuthorEmail",             EXTRACTOR_METATYPE_AUTHOR_EMAIL },
  { "_AuthorEmailDisplayName",  EXTRACTOR_METATYPE_AUTHOR_NAME },
  { "_EmailSubject",            EXTRACTOR_METATYPE_SUBJECT },
  { NULL, 0 }
};


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


/**
 * 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 mc 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 MsoContext *mc,
           const char *value)
{
  unsigned int i;
  char *dup;

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


/**
 * Report a meta data value, unless it is empty or a duplicate.
 *
 * @param mc 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 MsoContext *mc,
          enum EXTRACTOR_MetaType type,
          const char *value)
{
  char *tmp;
  char *val;
  char key[64];

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

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


/* ******************** minimal XML scanning ******************** */


/**
 * Append the UTF-8 encoding of @a cp to @a out at @a *off.
 *
 * @param out buffer to write to
 * @param off offset into @a out, updated
 * @param cp code point to encode
 */
static void
append_utf8 (char *out,
             size_t *off,
             unsigned int cp)
{
  size_t o = *off;

  if (cp < 0x80)
  {
    out[o++] = (char) cp;
  }
  else if (cp < 0x800)
  {
    out[o++] = (char) (0xC0 | (cp >> 6));
    out[o++] = (char) (0x80 | (cp & 0x3F));
  }
  else if (cp < 0x10000)
  {
    out[o++] = (char) (0xE0 | (cp >> 12));
    out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    out[o++] = (char) (0x80 | (cp & 0x3F));
  }
  else
  {
    out[o++] = (char) (0xF0 | (cp >> 18));
    out[o++] = (char) (0x80 | ((cp >> 12) & 0x3F));
    out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    out[o++] = (char) (0x80 | (cp & 0x3F));
  }
  *off = o;
}


/**
 * Resolve the XML entity starting at @a in (which points at the `&').
 *
 * @param in start of the entity
 * @param len number of bytes available at @a in
 * @param out buffer to append the replacement text to
 * @param off offset into @a out, updated on success
 * @return number of bytes consumed from @a in, 0 if this is not an
 *         entity reference we understand
 */
static size_t
resolve_entity (const char *in,
                size_t len,
                char *out,
                size_t *off)
{
  static const struct
  {
    const char *name;
    char value;
  } named[] = {
    { "amp;", '&' },
    { "lt;", '<' },
    { "gt;", '>' },
    { "quot;", '"' },
    { "apos;", '\'' },
    { NULL, 0 }
  };
  unsigned int i;
  unsigned long cp;
  size_t n;

  for (i = 0; NULL != named[i].name; i++)
  {
    size_t nl = strlen (named[i].name);

    if ( (len > nl) &&
         (0 == strncmp (in + 1, named[i].name, nl)) )
    {
      out[(*off)++] = named[i].value;
      return nl + 1;
    }
  }
  if ( (2 < len) &&
       ('#' == in[1]) )
  {
    char *end;

    if ( ('x' == in[2]) ||
         ('X' == in[2]) )
      cp = strtoul (in + 3, &end, 16);
    else
      cp = strtoul (in + 2, &end, 10);
    n = (size_t) (end - in);
    if ( (end > in + 2) &&
         (n < len) &&
         (';' == *end) &&
         (0 < cp) &&
         (0x10FFFF >= cp) )
    {
      append_utf8 (out, off, (unsigned int) cp);
      return n + 1;
    }
  }
  return 0;
}


/**
 * Copy @a len bytes from @a in to a fresh 0-terminated string,
 * resolving XML entity references on the way.  OOXML parts are UTF-8,
 * so no character set conversion is needed.
 *
 * @param in text to unescape
 * @param len number of bytes in @a in
 * @return the unescaped string to be freed by the caller, NULL on
 *         error or if @a len exceeds what we are willing to report
 */
static char *
xml_unescape (const char *in,
              size_t len)
{
  char *out;
  size_t i = 0;
  size_t o = 0;

  if (MAX_VALUE < len)
    return NULL;
  /* A resolved numeric reference is never longer than its source. */
  if (NULL == (out = malloc (len + 1)))
    return NULL;
  while (i < len)
  {
    if ('&' == in[i])
    {
      size_t used = resolve_entity (&in[i], len - i, out, &o);

      if (0 != used)
      {
        i += used;
        continue;
      }
    }
    out[o++] = in[i++];
  }
  out[o] = '\0';
  return out;
}


/**
 * Locate the value of attribute @a attr inside the XML tag @a tag.
 *
 * @param tag start of the tag (at the `<')
 * @param len length of the tag, including the `>'
 * @param attr name of the attribute to look for
 * @return the unescaped attribute value to be freed by the caller,
 *         NULL if the attribute is not present
 */
static char *
tag_attr (const char *tag,
          size_t len,
          const char *attr)
{
  size_t alen = strlen (attr);
  size_t i;

  for (i = 1; i + alen + 2 < len; i++)
  {
    const char *q;
    const char *end;

    /* The attribute name must be preceded by white space, so that
       looking for "name" does not match "displayName". */
    if (! isspace ((unsigned char) tag[i - 1]))
      continue;
    if (0 != strncmp (&tag[i], attr, alen))
      continue;
    q = &tag[i + alen];
    while ( (q < tag + len) &&
            (isspace ((unsigned char) *q)) )
      q++;
    if ( (q >= tag + len) ||
         ('=' != *q) )
      continue;
    q++;
    while ( (q < tag + len) &&
            (isspace ((unsigned char) *q)) )
      q++;
    if ( (q >= tag + len) ||
         ( ('"' != *q) &&
           ('\'' != *q) ) )
      continue;
    end = memchr (q + 1, *q, (size_t) (tag + len - q - 1));
    if (NULL == end)
      return NULL;
    return xml_unescape (q + 1, (size_t) (end - q - 1));
  }
  return NULL;
}


/**
 * Return the value of the first attribute from @a attrs that is
 * present in @a tag.
 *
 * @param tag start of the tag (at the `<')
 * @param len length of the tag, including the `>'
 * @param attrs NULL-terminated array of attribute names, in order of
 *        preference
 * @return the unescaped attribute value to be freed by the caller,
 *         NULL if none of the attributes is present
 */
static char *
tag_attr_any (const char *tag,
              size_t len,
              const char **attrs)
{
  unsigned int i;

  for (i = 0; NULL != attrs[i]; i++)
  {
    char *v = tag_attr (tag, len, attrs[i]);

    if (NULL != v)
      return v;
  }
  return NULL;
}


/**
 * Check whether the local name of the element in @a tag equals
 * @a name, ignoring any XML namespace prefix and case.
 *
 * @param tag start of the tag (at the `<')
 * @param len length of the tag, including the `>'
 * @param name local element name to compare against
 * @return 1 on match, 0 otherwise
 */
static int
tag_local_name_is (const char *tag,
                   size_t len,
                   const char *name)
{
  size_t start = 1;
  size_t i;
  size_t nlen = strlen (name);

  for (i = 1; i < len; i++)
  {
    if (':' == tag[i])
      start = i + 1;
    if ( (isspace ((unsigned char) tag[i])) ||
         ('/' == tag[i]) ||
         ('>' == tag[i]) )
      break;
  }
  if (i - start != nlen)
    return 0;
  for (i = 0; i < nlen; i++)
    if (tolower ((unsigned char) tag[start + i]) != tolower ((unsigned char)
                                                             name[i]))
      return 0;
  return 1;
}


/**
 * Extract the text content of every occurrence of element @a name in
 * @a buf and report it as @a type.
 *
 * @param mc our extraction state
 * @param buf 0-terminated XML document
 * @param name element name to look for, including any namespace prefix
 * @param type meta data type to report the content as
 */
static void
report_element (struct MsoContext *mc,
                const char *buf,
                const char *name,
                enum EXTRACTOR_MetaType type)
{
  char open[128];
  const char *p = buf;
  size_t nlen = strlen (name);

  if (sizeof (open) - 2 <= nlen)
    return;
  open[0] = '<';
  memcpy (&open[1], name, nlen + 1);
  while (0 == mc->stop)
  {
    const char *start;
    const char *gt;
    const char *close;
    char *value;

    if (NULL == (p = strstr (p, open)))
      return;
    start = p + 1 + nlen;
    /* The match must end the element name, not just prefix it. */
    if ( ('>' != start[0]) &&
         ('/' != start[0]) &&
         (! isspace ((unsigned char) start[0])) )
    {
      p = start;
      continue;
    }
    if (NULL == (gt = strchr (start, '>')))
      return;
    p = gt + 1;
    if ('/' == gt[-1])
      continue;   /* empty element */
    if (NULL == (close = strstr (p, "</")))
      return;
    if (NULL != (value = xml_unescape (p, (size_t) (close - p))))
    {
      add_meta (mc, type, value);
      free (value);
    }
    p = close;
  }
}


/**
 * Report the value of the custom document property @a name, if
 * present.  A custom property looks like
 * `<property ... name="_AuthorEmail"><vt:lpwstr>a@b</vt:lpwstr></property>'.
 *
 * @param mc our extraction state
 * @param buf 0-terminated XML document
 * @param name value of the `name' attribute to look for
 * @param type meta data type to report the value as
 */
static void
report_custom_property (struct MsoContext *mc,
                        const char *buf,
                        const char *name,
                        enum EXTRACTOR_MetaType type)
{
  char needle[128];
  const char *p;
  const char *gt;
  const char *lt;
  char *value;

  if (sizeof (needle) <= strlen (name) + 8)
    return;
  snprintf (needle, sizeof (needle), "name=\"%s\"", name);
  if (NULL == (p = strstr (buf, needle)))
    return;
  /* Skip to the end of the <property> tag, then into the typed value
     element that follows. */
  if (NULL == (gt = strchr (p, '>')))
    return;
  if (NULL == (p = strchr (gt + 1, '<')))
    return;
  if (NULL == (gt = strchr (p, '>')))
    return;
  if ('/' == gt[-1])
    return;
  if (NULL == (lt = strchr (gt + 1, '<')))
    return;
  if (NULL != (value = xml_unescape (gt + 1, (size_t) (lt - gt - 1))))
  {
    add_meta (mc, type, value);
    free (value);
  }
}


/* ******************** who edited the document ******************** */


/**
 * Attributes that carry the name of a person, in order of preference.
 * `w:author' is used by Word for tracked changes and comments,
 * `w15:author' by the Word 2013 people part, `userName' by the Excel
 * shared workbook revision log and `displayName' by the Excel persons
 * part.
 */
static const char *author_attrs[] = {
  "w:author",
  "w15:author",
  "userName",
  "displayName",
  NULL
};


/**
 * Attributes that carry the time of an edit.
 */
static const char *date_attrs[] = {
  "w:date",
  "w15:date",
  "dateTime",
  "p:dt",
  NULL
};


/**
 * Attributes that carry a directory identity -- an Active Directory
 * SID or, more often, the e-mail address the editor was signed in as.
 */
static const char *userid_attrs[] = {
  "w15:userId",
  "userId",
  NULL
};


/**
 * Inspect one XML tag for information about who edited the document.
 *
 * This deliberately keys off the attributes rather than off a list of
 * element names: Word alone marks up revisions with more than a dozen
 * different elements (`w:ins', `w:del', `w:moveFrom', `w:rPrChange',
 * `w:tcPrChange', ...), and they all carry the author and date the
 * same way.
 *
 * @param mc our extraction state
 * @param tag start of the tag (at the `<')
 * @param len length of the tag, including the `>'
 */
static void
handle_tag (struct MsoContext *mc,
            const char *tag,
            size_t len)
{
  char *author;
  char *date;
  char *userid;

  if ( (2 > len) ||
       ('/' == tag[1]) ||
       ('?' == tag[1]) ||
       ('!' == tag[1]) )
    return;
  author = tag_attr_any (tag, len, author_attrs);
  if (NULL == author)
  {
    /* PowerPoint keeps its comment authors in a list of <p:cmAuthor>
       (and, since PowerPoint 2016, <p188:author>) elements that name
       the person in a plain `name' attribute. */
    if ( (tag_local_name_is (tag, len, "cmAuthor")) ||
         (tag_local_name_is (tag, len, "author")) ||
         (tag_local_name_is (tag, len, "person")) )
      author = tag_attr (tag, len, "name");
  }
  userid = tag_attr_any (tag, len, userid_attrs);
  if ( (NULL == author) &&
       (NULL == userid) )
    return;
  date = tag_attr_any (tag, len, date_attrs);
  if (NULL != author)
    add_meta (mc, EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, author);
  if (NULL != userid)
    add_meta (mc, EXTRACTOR_METATYPE_CONTACT_INFORMATION, userid);
  if ( (NULL != author) &&
       (NULL != date) )
  {
    char *line;
    size_t bsize = strlen (author) + strlen (date) + 128;

    if (NULL != (line = malloc (bsize)))
    {
      int sret = snprintf (line,
                           bsize,
                           _ ("Author `%s' edited the document on `%s'"),
                           author,
                           date);

      if ( (0 < sret) &&
           (bsize > (size_t) sret) )
        add_meta (mc, EXTRACTOR_METATYPE_REVISION_HISTORY, line);
      free (line);
    }
  }
  free (author);
  free (date);
  free (userid);
}


/**
 * Scan a buffer of XML for tags and hand each of them to
 * #handle_tag().
 *
 * @param mc our extraction state
 * @param buf buffer to scan
 * @param len number of bytes in @a buf
 * @return number of bytes consumed; the remainder is an incomplete tag
 *         that the caller should present again with more data
 */
static size_t
scan_tags (struct MsoContext *mc,
           const char *buf,
           size_t len)
{
  const char *p = buf;
  const char *end = buf + len;

  while (p < end)
  {
    const char *lt;
    const char *gt;

    if (NULL == (lt = memchr (p, '<', (size_t) (end - p))))
      return len;
    if (NULL == (gt = memchr (lt, '>', (size_t) (end - lt))))
      return (size_t) (lt - buf);
    handle_tag (mc, lt, (size_t) (gt - lt + 1));
    if (0 != mc->stop)
      return len;
    p = gt + 1;
  }
  return len;
}


/* ******************** reading ZIP parts ******************** */


/**
 * Read the currently selected ZIP entry into a fresh 0-terminated
 * buffer.
 *
 * @param uf the ZIP archive
 * @param max maximum number of bytes to read
 * @param size set to the number of bytes read
 * @return the buffer to be freed by the caller, NULL on error
 */
static char *
read_current (struct EXTRACTOR_UnzipFile *uf,
              size_t max,
              size_t *size)
{
  struct EXTRACTOR_UnzipFileInfo fi;
  char *buf;
  ssize_t got;
  size_t want;

  if (EXTRACTOR_UNZIP_OK !=
      EXTRACTOR_common_unzip_get_current_file_info (uf, &fi, NULL, 0,
                                                    NULL, 0, NULL, 0))
    return NULL;
  want = fi.uncompressed_size;
  if (want > max)
    want = max;
  if (0 == want)
    return NULL;
  if (EXTRACTOR_UNZIP_OK !=
      EXTRACTOR_common_unzip_open_current_file (uf))
    return NULL;
  if (NULL == (buf = malloc (want + 1)))
  {
    EXTRACTOR_common_unzip_close_current_file (uf);
    return NULL;
  }
  got = EXTRACTOR_common_unzip_read_current_file (uf, buf, want);
  EXTRACTOR_common_unzip_close_current_file (uf);
  if (0 >= got)
  {
    free (buf);
    return NULL;
  }
  buf[got] = '\0';
  *size = (size_t) got;
  return buf;
}


/**
 * Read the part @a name into memory and return it 0-terminated.
 *
 * @param uf the ZIP archive
 * @param name name of the part
 * @return the buffer to be freed by the caller, NULL if the part is
 *         absent, empty or too large
 */
static char *
read_part (struct EXTRACTOR_UnzipFile *uf,
           const char *name)
{
  size_t size;

  if (EXTRACTOR_UNZIP_OK !=
      EXTRACTOR_common_unzip_go_find_local_file (uf, name, 2))
    return NULL;
  return read_current (uf, MAX_SMALL_PART, &size);
}


/**
 * Scan the part @a name for tags describing who edited the document.
 * The part is read through a sliding window, so that a part far too
 * large to hold in memory (`word/document.xml' of a heavily revised
 * document, or a decompression bomb) costs us a bounded amount of
 * memory.
 *
 * @param mc our extraction state
 * @param uf the ZIP archive
 * @param name name of the part to scan
 */
static void
scan_part (struct MsoContext *mc,
           struct EXTRACTOR_UnzipFile *uf,
           const char *name)
{
  char *buf;
  size_t carry = 0;
  uint64_t total = 0;

  if (EXTRACTOR_UNZIP_OK !=
      EXTRACTOR_common_unzip_go_find_local_file (uf, name, 2))
    return;
  if (EXTRACTOR_UNZIP_OK !=
      EXTRACTOR_common_unzip_open_current_file (uf))
    return;
  /* One byte of slack so that the window is always 0-terminated: the
     attribute values we hand to xml_unescape() point into it. */
  if (NULL == (buf = malloc (SCAN_CHUNK + 1)))
  {
    EXTRACTOR_common_unzip_close_current_file (uf);
    return;
  }
  while (0 == mc->stop)
  {
    ssize_t got;
    size_t have;
    size_t used;

    got = EXTRACTOR_common_unzip_read_current_file (uf,
                                                    &buf[carry],
                                                    SCAN_CHUNK - carry);
    if (0 >= got)
      break;
    have = carry + (size_t) got;
    total += (uint64_t) got;
    buf[have] = '\0';
    used = scan_tags (mc, buf, have);
    carry = have - used;
    if (MAX_TAG < carry)
      carry = 0;   /* absurdly long tag; resynchronise */
    if (0 != carry)
      memmove (buf, &buf[used], carry);
    if (MAX_SCAN_BYTES < total)
      break;
  }
  free (buf);
  EXTRACTOR_common_unzip_close_current_file (uf);
}


/**
 * Does @a name start with @a prefix?
 *
 * @param name string to test
 * @param prefix prefix to test for
 * @return 1 on match, 0 otherwise
 */
static int
has_prefix (const char *name,
            const char *prefix)
{
  return 0 == strncmp (name, prefix, strlen (prefix));
}


/**
 * Does @a name end with @a suffix?
 *
 * @param name string to test
 * @param suffix suffix to test for
 * @return 1 on match, 0 otherwise
 */
static int
has_suffix (const char *name,
            const char *suffix)
{
  size_t nl = strlen (name);
  size_t sl = strlen (suffix);

  return (nl >= sl) && (0 == strcmp (&name[nl - sl], suffix));
}


/**
 * Is @a name a part that can name the people who worked on the
 * document?
 *
 * @param name name of a part inside the OOXML package
 * @return 1 if the part is worth scanning, 0 otherwise
 */
static int
is_people_part (const char *name)
{
  if (! has_suffix (name, ".xml"))
    return 0;
  /* Word: tracked changes live in the document body and in every
     story around it; comments and the identities behind them live in
     parts of their own. */
  if ( (0 == strcmp (name, "word/document.xml")) ||
       (0 == strcmp (name, "word/footnotes.xml")) ||
       (0 == strcmp (name, "word/endnotes.xml")) ||
       (0 == strcmp (name, "word/comments.xml")) ||
       (0 == strcmp (name, "word/commentsExtended.xml")) ||
       (0 == strcmp (name, "word/people.xml")) ||
       (has_prefix (name, "word/header")) ||
       (has_prefix (name, "word/footer")) )
    return 1;
  /* Excel: the workbook part carries <fileSharing userName="...">,
     the direct descendant of the BIFF FILESHARING record. */
  if (0 == strcmp (name, "xl/workbook.xml"))
    return 1;
  /* Excel: the shared workbook revision log, the modern threaded
     comment authors and the classic comment author list. */
  if ( (has_prefix (name, "xl/revisions/")) ||
       (has_prefix (name, "xl/persons/")) ||
       (has_prefix (name, "xl/threadedComments/")) ||
       (has_prefix (name, "xl/comments")) )
    return 1;
  /* PowerPoint: the comment author list and the comments themselves. */
  if ( (0 == strcmp (name, "ppt/commentAuthors.xml")) ||
       (0 == strcmp (name, "ppt/authors.xml")) ||
       (has_prefix (name, "ppt/comments/")) )
    return 1;
  return 0;
}


/* ******************** the two formats ******************** */


/**
 * Extract meta data from an Office Open XML package.
 *
 * @param mc our extraction state
 * @return 1 if the file was an OOXML package, 0 if not
 */
static int
extract_ooxml (struct MsoContext *mc)
{
  struct EXTRACTOR_UnzipFile *uf;
  char *parts[MAX_PARTS];
  unsigned int num_parts = 0;
  unsigned int i;
  const char *mime = NULL;
  char *buf;
  int ret;

  if (NULL == (uf = EXTRACTOR_common_unzip_open (mc->ec)))
    return 0;
  /* First pass: learn what kind of document this is and which parts
     are worth looking at.  We collect the names instead of extracting
     as we iterate, so that seeking around the archive cannot disturb
     the walk over the central directory. */
  ret = EXTRACTOR_common_unzip_go_to_first_file (uf);
  while (EXTRACTOR_UNZIP_OK == ret)
  {
    char name[MAXFILENAME];

    if (EXTRACTOR_UNZIP_OK ==
        EXTRACTOR_common_unzip_get_current_file_info (uf, NULL,
                                                      name, sizeof (name),
                                                      NULL, 0, NULL, 0))
    {
      name[sizeof (name) - 1] = '\0';
      if (0 == strcmp (name, "word/document.xml"))
        mime =
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
      else if (0 == strcmp (name, "xl/workbook.xml"))
        mime =
          "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
      else if (0 == strcmp (name, "ppt/presentation.xml"))
        mime =
          "application/vnd.openxmlformats-officedocument.presentationml.presentation";
      if ( (is_people_part (name)) &&
           (MAX_PARTS > num_parts) )
      {
        char *dup = strdup (name);

        if (NULL != dup)
          parts[num_parts++] = dup;
      }
    }
    ret = EXTRACTOR_common_unzip_go_to_next_file (uf);
  }
  if (NULL == mime)
  {
    /* Not an OOXML document -- most likely an ODF file or a plain ZIP,
       both of which have plugins of their own. */
    for (i = 0; i < num_parts; i++)
      free (parts[i]);
    EXTRACTOR_common_unzip_close (uf);
    return 0;
  }
  add_meta (mc, EXTRACTOR_METATYPE_MIMETYPE, mime);

  if (NULL != (buf = read_part (uf, "docProps/core.xml")))
  {
    for (i = 0; NULL != core_map[i].text; i++)
      report_element (mc, buf, core_map[i].text, core_map[i].type);
    free (buf);
  }
  if (NULL != (buf = read_part (uf, "docProps/app.xml")))
  {
    for (i = 0; NULL != app_map[i].text; i++)
      report_element (mc, buf, app_map[i].text, app_map[i].type);
    free (buf);
  }
  if (NULL != (buf = read_part (uf, "docProps/custom.xml")))
  {
    for (i = 0; NULL != custom_map[i].text; i++)
      report_custom_property (mc, buf, custom_map[i].text, custom_map[i].type);
    free (buf);
  }
  for (i = 0; (i < num_parts) && (0 == mc->stop); i++)
  {
    /* Excel keeps the authors of classic cell comments in a list of
       <author> elements rather than in an attribute. */
    if (has_prefix (parts[i], "xl/comments"))
    {
      if (NULL != (buf = read_part (uf, parts[i])))
      {
        report_element (mc, buf, "author",
                        EXTRACTOR_METATYPE_CONTRIBUTOR_NAME);
        free (buf);
      }
    }
    scan_part (mc, uf, parts[i]);
  }
  for (i = 0; i < num_parts; i++)
    free (parts[i]);
  EXTRACTOR_common_unzip_close (uf);
  return 1;
}


/**
 * Extract meta data from a bare BIFF stream, as written by Excel 2 to
 * Excel 4 (and still produced by some exporters).
 *
 * @param mc our extraction state
 * @return 1 if the file was a BIFF stream, 0 if not
 */
static int
extract_biff (struct MsoContext *mc)
{
  struct EXTRACTOR_ExtractContext *ec = mc->ec;
  void *data;
  ssize_t avail;
  uint64_t fsize;

  fsize = ec->get_size (ec->cls);
  if ( (8 > fsize) ||
       (0 != ec->seek (ec->cls, 0, SEEK_SET)) )
    return 0;
  /* The File Protection Block is the very first thing in the stream,
     so a small window is enough. */
  avail = ec->read (ec->cls,
                    &data,
                    (fsize < 64 * 1024) ? (size_t) fsize : 64 * 1024);
  if (8 > avail)
    return 0;
  if (0 > EXTRACTOR_msoffice_biff_extract ((const unsigned char *) data,
                                           (size_t) avail,
                                           PLUGIN_NAME,
                                           ec->proc,
                                           ec->cls))
    return 0;
  add_meta (mc, EXTRACTOR_METATYPE_MIMETYPE, "application/vnd.ms-excel");
  return 1;
}


/**
 * Main entry method for the MS Office extraction plugin.
 *
 * @param ec extraction context provided to the plugin
 */
void
EXTRACTOR_msoffice_extract_method (struct EXTRACTOR_ExtractContext *ec);


void
EXTRACTOR_msoffice_extract_method (struct EXTRACTOR_ExtractContext *ec)
{
  struct MsoContext mc;
  unsigned int i;
  void *data;
  ssize_t avail;

  memset (&mc, 0, sizeof (mc));
  mc.ec = ec;
  if (0 != ec->seek (ec->cls, 0, SEEK_SET))
    return;
  if (4 > (avail = ec->read (ec->cls, &data, 4)))
    return;
  if (0 == memcmp (data, "PK\003\004", 4))
    extract_ooxml (&mc);
  else
    extract_biff (&mc);
  for (i = 0; i < mc.seen_len; i++)
    free (mc.seen[i]);
}


/* end of msoffice_extractor.c */
