libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

msoffice_extractor.c (32844B)


      1 /*
      2      This file is part of libextractor.
      3      Copyright (C) 2026 Vidyut Samanta and Christian Grothoff
      4 
      5      libextractor is free software; you can redistribute it and/or modify
      6      it under the terms of the GNU General Public License as published
      7      by the Free Software Foundation; either version 3, or (at your
      8      option) any later version.
      9 
     10      libextractor is distributed in the hope that it will be useful, but
     11      WITHOUT ANY WARRANTY; without even the implied warranty of
     12      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13      General Public License for more details.
     14 
     15      You should have received a copy of the GNU General Public License
     16      along with libextractor; see the file COPYING.  If not, write to the
     17      Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     18      Boston, MA 02110-1301, USA.
     19  */
     20 /**
     21  * @file plugins/msoffice_extractor.c
     22  * @brief plugin to support Microsoft Office documents that are not
     23  *        stored in an OLE2 container
     24  * @author Christian Grothoff
     25  *
     26  * The `ole2' plugin covers the OLE2-based generation of Microsoft
     27  * Office formats (.doc, .xls, .ppt) and needs libgsf to open the
     28  * container.  This plugin covers the two families that live outside of
     29  * an OLE2 container and hence need no libgsf at all:
     30  *
     31  * - Office Open XML (.docx, .xlsx, .pptx and their macro-enabled and
     32  *   template variants), which is a ZIP archive of XML parts.
     33  * - Bare BIFF record streams, which is how Excel 2 to Excel 4 wrote
     34  *   .xls files before OLE2 existed.  These are still common.
     35  *
     36  * Beyond the document summary, both carry a considerable amount of
     37  * information about *who* edited a document and *when*: tracked
     38  * changes, comment authors, the identities used to author them,
     39  * shared-workbook revision logs, the "send for review" e-mail
     40  * addresses left behind by Outlook, and the user name recorded in the
     41  * Excel file protection records.  Extracting this is the point of
     42  * https://bugs.gnunet.org/view.php?id=2096 -- it matters both for
     43  * forensics and for privacy tools that want to warn about the
     44  * information a document leaks.
     45  */
     46 #include "platform.h"
     47 #include <ctype.h>
     48 #include "extractor.h"
     49 #include "convert.h"
     50 #include "unzip.h"
     51 #include "msoffice_biff.h"
     52 
     53 
     54 /**
     55  * Name this plugin reports itself as.
     56  */
     57 #define PLUGIN_NAME "msoffice"
     58 
     59 /**
     60  * Maximum length of a file name inside the ZIP archive.
     61  */
     62 #define MAXFILENAME 256
     63 
     64 /**
     65  * Maximum number of interesting ZIP entries we remember.  Documents
     66  * with more comment or revision parts than this are pathological.
     67  */
     68 #define MAX_PARTS 64
     69 
     70 /**
     71  * Maximum size of a part that we read into memory as a whole.  Only
     72  * used for the small property parts.
     73  */
     74 #define MAX_SMALL_PART (256 * 1024)
     75 
     76 /**
     77  * Size of the sliding window used to scan the large parts.
     78  */
     79 #define SCAN_CHUNK (128 * 1024)
     80 
     81 /**
     82  * Longest XML tag we are able to look at in one piece.  Tags carrying
     83  * an author are far shorter; anything longer is skipped rather than
     84  * buffered, which bounds our memory use on hostile input.
     85  */
     86 #define MAX_TAG 8192
     87 
     88 /**
     89  * Maximum number of bytes we scan in a single part.  Guards against
     90  * decompression bombs.
     91  */
     92 #define MAX_SCAN_BYTES (64 * 1024 * 1024)
     93 
     94 /**
     95  * Maximum number of distinct strings we remember to suppress
     96  * duplicates.  A document with tracked changes typically repeats the
     97  * same handful of authors thousands of times.
     98  */
     99 #define MAX_SEEN 256
    100 
    101 /**
    102  * Longest metadata value we report.
    103  */
    104 #define MAX_VALUE 4096
    105 
    106 
    107 /**
    108  * State kept while extracting from one document.
    109  */
    110 struct MsoContext
    111 {
    112   /**
    113    * Extraction context we were called with.
    114    */
    115   struct EXTRACTOR_ExtractContext *ec;
    116 
    117   /**
    118    * Strings we have already reported, to suppress duplicates.
    119    */
    120   char *seen[MAX_SEEN];
    121 
    122   /**
    123    * Number of used entries in @e seen.
    124    */
    125   unsigned int seen_len;
    126 
    127   /**
    128    * Set to 1 once the caller asked us to stop.
    129    */
    130   int stop;
    131 };
    132 
    133 
    134 /**
    135  * Entry in a map from XML element names to LE meta data types.
    136  */
    137 struct Matches
    138 {
    139   /**
    140    * Name of the XML element.
    141    */
    142   const char *text;
    143 
    144   /**
    145    * Corresponding LE type.
    146    */
    147   enum EXTRACTOR_MetaType type;
    148 };
    149 
    150 
    151 /**
    152  * Elements of the OPC core properties part (`docProps/core.xml'),
    153  * which is the Dublin Core derived summary of the document.
    154  */
    155 static struct Matches core_map[] = {
    156   { "dc:title",           EXTRACTOR_METATYPE_TITLE },
    157   { "dc:subject",         EXTRACTOR_METATYPE_SUBJECT },
    158   { "dc:creator",         EXTRACTOR_METATYPE_CREATOR },
    159   { "dc:description",     EXTRACTOR_METATYPE_DESCRIPTION },
    160   { "dc:language",        EXTRACTOR_METATYPE_LANGUAGE },
    161   { "cp:keywords",        EXTRACTOR_METATYPE_KEYWORDS },
    162   { "cp:category",        EXTRACTOR_METATYPE_SECTION },
    163   { "cp:lastModifiedBy",  EXTRACTOR_METATYPE_LAST_SAVED_BY },
    164   { "cp:revision",        EXTRACTOR_METATYPE_REVISION_NUMBER },
    165   { "cp:lastPrinted",     EXTRACTOR_METATYPE_LAST_PRINTED },
    166   { "dcterms:created",    EXTRACTOR_METATYPE_CREATION_DATE },
    167   { "dcterms:modified",   EXTRACTOR_METATYPE_MODIFICATION_DATE },
    168   { NULL, 0 }
    169 };
    170 
    171 
    172 /**
    173  * Elements of the extended properties part (`docProps/app.xml'),
    174  * which is written by the application that saved the document.
    175  */
    176 static struct Matches app_map[] = {
    177   { "Application",    EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE },
    178   { "AppVersion",     EXTRACTOR_METATYPE_SOFTWARE_VERSION },
    179   { "Company",        EXTRACTOR_METATYPE_COMPANY },
    180   { "Manager",        EXTRACTOR_METATYPE_MANAGER },
    181   { "Template",       EXTRACTOR_METATYPE_TEMPLATE },
    182   { "TotalTime",      EXTRACTOR_METATYPE_TOTAL_EDITING_TIME },
    183   { "Pages",          EXTRACTOR_METATYPE_PAGE_COUNT },
    184   { "Words",          EXTRACTOR_METATYPE_WORD_COUNT },
    185   { "Characters",     EXTRACTOR_METATYPE_CHARACTER_COUNT },
    186   { "Lines",          EXTRACTOR_METATYPE_LINE_COUNT },
    187   { "Paragraphs",     EXTRACTOR_METATYPE_PARAGRAPH_COUNT },
    188   { NULL, 0 }
    189 };
    190 
    191 
    192 /**
    193  * Custom properties (`docProps/custom.xml') that carry personal
    194  * information.  Word writes these when a document is sent around for
    195  * review from Outlook; they routinely outlive the review and identify
    196  * the person who circulated the document.
    197  */
    198 static struct Matches custom_map[] = {
    199   { "_AuthorEmail",             EXTRACTOR_METATYPE_AUTHOR_EMAIL },
    200   { "_AuthorEmailDisplayName",  EXTRACTOR_METATYPE_AUTHOR_NAME },
    201   { "_EmailSubject",            EXTRACTOR_METATYPE_SUBJECT },
    202   { NULL, 0 }
    203 };
    204 
    205 
    206 /* ******************** generic helpers ******************** */
    207 
    208 
    209 /**
    210  * Trim leading and trailing white space in @a s, in place.
    211  *
    212  * @param s 0-terminated string to trim
    213  * @return pointer into @a s to the first non-blank character
    214  */
    215 static char *
    216 trim (char *s)
    217 {
    218   size_t len = strlen (s);
    219 
    220   while ( (0 < len) &&
    221           (isspace ((unsigned char) s[len - 1])) )
    222     s[--len] = '\0';
    223   while (isspace ((unsigned char) s[0]))
    224     s++;
    225   return s;
    226 }
    227 
    228 
    229 /**
    230  * Check whether @a value was reported before, and remember it if not.
    231  *
    232  * @param mc our extraction state
    233  * @param value string to check
    234  * @return 1 if @a value is new (and was remembered), 0 if it is a
    235  *         duplicate or if we ran out of space to remember it
    236  */
    237 static int
    238 mark_seen (struct MsoContext *mc,
    239            const char *value)
    240 {
    241   unsigned int i;
    242   char *dup;
    243 
    244   for (i = 0; i < mc->seen_len; i++)
    245     if (0 == strcmp (mc->seen[i], value))
    246       return 0;
    247   if (MAX_SEEN == mc->seen_len)
    248     return 0;
    249   if (NULL == (dup = strdup (value)))
    250     return 0;
    251   mc->seen[mc->seen_len++] = dup;
    252   return 1;
    253 }
    254 
    255 
    256 /**
    257  * Report a meta data value, unless it is empty or a duplicate.
    258  *
    259  * @param mc our extraction state
    260  * @param type meta data type to report the value as
    261  * @param value the value; leading and trailing white space is removed
    262  */
    263 static void
    264 add_meta (struct MsoContext *mc,
    265           enum EXTRACTOR_MetaType type,
    266           const char *value)
    267 {
    268   char *tmp;
    269   char *val;
    270   char key[64];
    271 
    272   if ( (0 != mc->stop) ||
    273        (NULL == value) )
    274     return;
    275   if (MAX_VALUE < strlen (value))
    276     return;
    277   if (NULL == (tmp = strdup (value)))
    278     return;
    279   val = trim (tmp);
    280   if ('\0' == val[0])
    281   {
    282     free (tmp);
    283     return;
    284   }
    285   /* De-duplicate per type: the same name legitimately shows up as both
    286      a creator and a comment author, and both are worth reporting. */
    287   if ( (0 < snprintf (key, sizeof (key), "%d:", (int) type)) &&
    288        (strlen (key) + strlen (val) < MAX_VALUE) )
    289   {
    290     char full[MAX_VALUE + 64];
    291 
    292     snprintf (full, sizeof (full), "%d:%s", (int) type, val);
    293     if (0 == mark_seen (mc, full))
    294     {
    295       free (tmp);
    296       return;
    297     }
    298   }
    299   if (0 != mc->ec->proc (mc->ec->cls,
    300                          PLUGIN_NAME,
    301                          type,
    302                          EXTRACTOR_METAFORMAT_UTF8,
    303                          "text/plain",
    304                          val,
    305                          strlen (val) + 1))
    306     mc->stop = 1;
    307   free (tmp);
    308 }
    309 
    310 
    311 /* ******************** minimal XML scanning ******************** */
    312 
    313 
    314 /**
    315  * Append the UTF-8 encoding of @a cp to @a out at @a *off.
    316  *
    317  * @param out buffer to write to
    318  * @param off offset into @a out, updated
    319  * @param cp code point to encode
    320  */
    321 static void
    322 append_utf8 (char *out,
    323              size_t *off,
    324              unsigned int cp)
    325 {
    326   size_t o = *off;
    327 
    328   if (cp < 0x80)
    329   {
    330     out[o++] = (char) cp;
    331   }
    332   else if (cp < 0x800)
    333   {
    334     out[o++] = (char) (0xC0 | (cp >> 6));
    335     out[o++] = (char) (0x80 | (cp & 0x3F));
    336   }
    337   else if (cp < 0x10000)
    338   {
    339     out[o++] = (char) (0xE0 | (cp >> 12));
    340     out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    341     out[o++] = (char) (0x80 | (cp & 0x3F));
    342   }
    343   else
    344   {
    345     out[o++] = (char) (0xF0 | (cp >> 18));
    346     out[o++] = (char) (0x80 | ((cp >> 12) & 0x3F));
    347     out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    348     out[o++] = (char) (0x80 | (cp & 0x3F));
    349   }
    350   *off = o;
    351 }
    352 
    353 
    354 /**
    355  * Resolve the XML entity starting at @a in (which points at the `&').
    356  *
    357  * @param in start of the entity
    358  * @param len number of bytes available at @a in
    359  * @param out buffer to append the replacement text to
    360  * @param off offset into @a out, updated on success
    361  * @return number of bytes consumed from @a in, 0 if this is not an
    362  *         entity reference we understand
    363  */
    364 static size_t
    365 resolve_entity (const char *in,
    366                 size_t len,
    367                 char *out,
    368                 size_t *off)
    369 {
    370   static const struct
    371   {
    372     const char *name;
    373     char value;
    374   } named[] = {
    375     { "amp;", '&' },
    376     { "lt;", '<' },
    377     { "gt;", '>' },
    378     { "quot;", '"' },
    379     { "apos;", '\'' },
    380     { NULL, 0 }
    381   };
    382   unsigned int i;
    383   unsigned long cp;
    384   size_t n;
    385 
    386   for (i = 0; NULL != named[i].name; i++)
    387   {
    388     size_t nl = strlen (named[i].name);
    389 
    390     if ( (len > nl) &&
    391          (0 == strncmp (in + 1, named[i].name, nl)) )
    392     {
    393       out[(*off)++] = named[i].value;
    394       return nl + 1;
    395     }
    396   }
    397   if ( (2 < len) &&
    398        ('#' == in[1]) )
    399   {
    400     char *end;
    401 
    402     if ( ('x' == in[2]) ||
    403          ('X' == in[2]) )
    404       cp = strtoul (in + 3, &end, 16);
    405     else
    406       cp = strtoul (in + 2, &end, 10);
    407     n = (size_t) (end - in);
    408     if ( (end > in + 2) &&
    409          (n < len) &&
    410          (';' == *end) &&
    411          (0 < cp) &&
    412          (0x10FFFF >= cp) )
    413     {
    414       append_utf8 (out, off, (unsigned int) cp);
    415       return n + 1;
    416     }
    417   }
    418   return 0;
    419 }
    420 
    421 
    422 /**
    423  * Copy @a len bytes from @a in to a fresh 0-terminated string,
    424  * resolving XML entity references on the way.  OOXML parts are UTF-8,
    425  * so no character set conversion is needed.
    426  *
    427  * @param in text to unescape
    428  * @param len number of bytes in @a in
    429  * @return the unescaped string to be freed by the caller, NULL on
    430  *         error or if @a len exceeds what we are willing to report
    431  */
    432 static char *
    433 xml_unescape (const char *in,
    434               size_t len)
    435 {
    436   char *out;
    437   size_t i = 0;
    438   size_t o = 0;
    439 
    440   if (MAX_VALUE < len)
    441     return NULL;
    442   /* A resolved numeric reference is never longer than its source. */
    443   if (NULL == (out = malloc (len + 1)))
    444     return NULL;
    445   while (i < len)
    446   {
    447     if ('&' == in[i])
    448     {
    449       size_t used = resolve_entity (&in[i], len - i, out, &o);
    450 
    451       if (0 != used)
    452       {
    453         i += used;
    454         continue;
    455       }
    456     }
    457     out[o++] = in[i++];
    458   }
    459   out[o] = '\0';
    460   return out;
    461 }
    462 
    463 
    464 /**
    465  * Locate the value of attribute @a attr inside the XML tag @a tag.
    466  *
    467  * @param tag start of the tag (at the `<')
    468  * @param len length of the tag, including the `>'
    469  * @param attr name of the attribute to look for
    470  * @return the unescaped attribute value to be freed by the caller,
    471  *         NULL if the attribute is not present
    472  */
    473 static char *
    474 tag_attr (const char *tag,
    475           size_t len,
    476           const char *attr)
    477 {
    478   size_t alen = strlen (attr);
    479   size_t i;
    480 
    481   for (i = 1; i + alen + 2 < len; i++)
    482   {
    483     const char *q;
    484     const char *end;
    485 
    486     /* The attribute name must be preceded by white space, so that
    487        looking for "name" does not match "displayName". */
    488     if (! isspace ((unsigned char) tag[i - 1]))
    489       continue;
    490     if (0 != strncmp (&tag[i], attr, alen))
    491       continue;
    492     q = &tag[i + alen];
    493     while ( (q < tag + len) &&
    494             (isspace ((unsigned char) *q)) )
    495       q++;
    496     if ( (q >= tag + len) ||
    497          ('=' != *q) )
    498       continue;
    499     q++;
    500     while ( (q < tag + len) &&
    501             (isspace ((unsigned char) *q)) )
    502       q++;
    503     if ( (q >= tag + len) ||
    504          ( ('"' != *q) &&
    505            ('\'' != *q) ) )
    506       continue;
    507     end = memchr (q + 1, *q, (size_t) (tag + len - q - 1));
    508     if (NULL == end)
    509       return NULL;
    510     return xml_unescape (q + 1, (size_t) (end - q - 1));
    511   }
    512   return NULL;
    513 }
    514 
    515 
    516 /**
    517  * Return the value of the first attribute from @a attrs that is
    518  * present in @a tag.
    519  *
    520  * @param tag start of the tag (at the `<')
    521  * @param len length of the tag, including the `>'
    522  * @param attrs NULL-terminated array of attribute names, in order of
    523  *        preference
    524  * @return the unescaped attribute value to be freed by the caller,
    525  *         NULL if none of the attributes is present
    526  */
    527 static char *
    528 tag_attr_any (const char *tag,
    529               size_t len,
    530               const char **attrs)
    531 {
    532   unsigned int i;
    533 
    534   for (i = 0; NULL != attrs[i]; i++)
    535   {
    536     char *v = tag_attr (tag, len, attrs[i]);
    537 
    538     if (NULL != v)
    539       return v;
    540   }
    541   return NULL;
    542 }
    543 
    544 
    545 /**
    546  * Check whether the local name of the element in @a tag equals
    547  * @a name, ignoring any XML namespace prefix and case.
    548  *
    549  * @param tag start of the tag (at the `<')
    550  * @param len length of the tag, including the `>'
    551  * @param name local element name to compare against
    552  * @return 1 on match, 0 otherwise
    553  */
    554 static int
    555 tag_local_name_is (const char *tag,
    556                    size_t len,
    557                    const char *name)
    558 {
    559   size_t start = 1;
    560   size_t i;
    561   size_t nlen = strlen (name);
    562 
    563   for (i = 1; i < len; i++)
    564   {
    565     if (':' == tag[i])
    566       start = i + 1;
    567     if ( (isspace ((unsigned char) tag[i])) ||
    568          ('/' == tag[i]) ||
    569          ('>' == tag[i]) )
    570       break;
    571   }
    572   if (i - start != nlen)
    573     return 0;
    574   for (i = 0; i < nlen; i++)
    575     if (tolower ((unsigned char) tag[start + i]) != tolower ((unsigned char)
    576                                                              name[i]))
    577       return 0;
    578   return 1;
    579 }
    580 
    581 
    582 /**
    583  * Extract the text content of every occurrence of element @a name in
    584  * @a buf and report it as @a type.
    585  *
    586  * @param mc our extraction state
    587  * @param buf 0-terminated XML document
    588  * @param name element name to look for, including any namespace prefix
    589  * @param type meta data type to report the content as
    590  */
    591 static void
    592 report_element (struct MsoContext *mc,
    593                 const char *buf,
    594                 const char *name,
    595                 enum EXTRACTOR_MetaType type)
    596 {
    597   char open[128];
    598   const char *p = buf;
    599   size_t nlen = strlen (name);
    600 
    601   if (sizeof (open) - 2 <= nlen)
    602     return;
    603   open[0] = '<';
    604   memcpy (&open[1], name, nlen + 1);
    605   while (0 == mc->stop)
    606   {
    607     const char *start;
    608     const char *gt;
    609     const char *close;
    610     char *value;
    611 
    612     if (NULL == (p = strstr (p, open)))
    613       return;
    614     start = p + 1 + nlen;
    615     /* The match must end the element name, not just prefix it. */
    616     if ( ('>' != start[0]) &&
    617          ('/' != start[0]) &&
    618          (! isspace ((unsigned char) start[0])) )
    619     {
    620       p = start;
    621       continue;
    622     }
    623     if (NULL == (gt = strchr (start, '>')))
    624       return;
    625     p = gt + 1;
    626     if ('/' == gt[-1])
    627       continue;   /* empty element */
    628     if (NULL == (close = strstr (p, "</")))
    629       return;
    630     if (NULL != (value = xml_unescape (p, (size_t) (close - p))))
    631     {
    632       add_meta (mc, type, value);
    633       free (value);
    634     }
    635     p = close;
    636   }
    637 }
    638 
    639 
    640 /**
    641  * Report the value of the custom document property @a name, if
    642  * present.  A custom property looks like
    643  * `<property ... name="_AuthorEmail"><vt:lpwstr>a@b</vt:lpwstr></property>'.
    644  *
    645  * @param mc our extraction state
    646  * @param buf 0-terminated XML document
    647  * @param name value of the `name' attribute to look for
    648  * @param type meta data type to report the value as
    649  */
    650 static void
    651 report_custom_property (struct MsoContext *mc,
    652                         const char *buf,
    653                         const char *name,
    654                         enum EXTRACTOR_MetaType type)
    655 {
    656   char needle[128];
    657   const char *p;
    658   const char *gt;
    659   const char *lt;
    660   char *value;
    661 
    662   if (sizeof (needle) <= strlen (name) + 8)
    663     return;
    664   snprintf (needle, sizeof (needle), "name=\"%s\"", name);
    665   if (NULL == (p = strstr (buf, needle)))
    666     return;
    667   /* Skip to the end of the <property> tag, then into the typed value
    668      element that follows. */
    669   if (NULL == (gt = strchr (p, '>')))
    670     return;
    671   if (NULL == (p = strchr (gt + 1, '<')))
    672     return;
    673   if (NULL == (gt = strchr (p, '>')))
    674     return;
    675   if ('/' == gt[-1])
    676     return;
    677   if (NULL == (lt = strchr (gt + 1, '<')))
    678     return;
    679   if (NULL != (value = xml_unescape (gt + 1, (size_t) (lt - gt - 1))))
    680   {
    681     add_meta (mc, type, value);
    682     free (value);
    683   }
    684 }
    685 
    686 
    687 /* ******************** who edited the document ******************** */
    688 
    689 
    690 /**
    691  * Attributes that carry the name of a person, in order of preference.
    692  * `w:author' is used by Word for tracked changes and comments,
    693  * `w15:author' by the Word 2013 people part, `userName' by the Excel
    694  * shared workbook revision log and `displayName' by the Excel persons
    695  * part.
    696  */
    697 static const char *author_attrs[] = {
    698   "w:author",
    699   "w15:author",
    700   "userName",
    701   "displayName",
    702   NULL
    703 };
    704 
    705 
    706 /**
    707  * Attributes that carry the time of an edit.
    708  */
    709 static const char *date_attrs[] = {
    710   "w:date",
    711   "w15:date",
    712   "dateTime",
    713   "p:dt",
    714   NULL
    715 };
    716 
    717 
    718 /**
    719  * Attributes that carry a directory identity -- an Active Directory
    720  * SID or, more often, the e-mail address the editor was signed in as.
    721  */
    722 static const char *userid_attrs[] = {
    723   "w15:userId",
    724   "userId",
    725   NULL
    726 };
    727 
    728 
    729 /**
    730  * Inspect one XML tag for information about who edited the document.
    731  *
    732  * This deliberately keys off the attributes rather than off a list of
    733  * element names: Word alone marks up revisions with more than a dozen
    734  * different elements (`w:ins', `w:del', `w:moveFrom', `w:rPrChange',
    735  * `w:tcPrChange', ...), and they all carry the author and date the
    736  * same way.
    737  *
    738  * @param mc our extraction state
    739  * @param tag start of the tag (at the `<')
    740  * @param len length of the tag, including the `>'
    741  */
    742 static void
    743 handle_tag (struct MsoContext *mc,
    744             const char *tag,
    745             size_t len)
    746 {
    747   char *author;
    748   char *date;
    749   char *userid;
    750 
    751   if ( (2 > len) ||
    752        ('/' == tag[1]) ||
    753        ('?' == tag[1]) ||
    754        ('!' == tag[1]) )
    755     return;
    756   author = tag_attr_any (tag, len, author_attrs);
    757   if (NULL == author)
    758   {
    759     /* PowerPoint keeps its comment authors in a list of <p:cmAuthor>
    760        (and, since PowerPoint 2016, <p188:author>) elements that name
    761        the person in a plain `name' attribute. */
    762     if ( (tag_local_name_is (tag, len, "cmAuthor")) ||
    763          (tag_local_name_is (tag, len, "author")) ||
    764          (tag_local_name_is (tag, len, "person")) )
    765       author = tag_attr (tag, len, "name");
    766   }
    767   userid = tag_attr_any (tag, len, userid_attrs);
    768   if ( (NULL == author) &&
    769        (NULL == userid) )
    770     return;
    771   date = tag_attr_any (tag, len, date_attrs);
    772   if (NULL != author)
    773     add_meta (mc, EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, author);
    774   if (NULL != userid)
    775     add_meta (mc, EXTRACTOR_METATYPE_CONTACT_INFORMATION, userid);
    776   if ( (NULL != author) &&
    777        (NULL != date) )
    778   {
    779     char *line;
    780     size_t bsize = strlen (author) + strlen (date) + 128;
    781 
    782     if (NULL != (line = malloc (bsize)))
    783     {
    784       int sret = snprintf (line,
    785                            bsize,
    786                            _ ("Author `%s' edited the document on `%s'"),
    787                            author,
    788                            date);
    789 
    790       if ( (0 < sret) &&
    791            (bsize > (size_t) sret) )
    792         add_meta (mc, EXTRACTOR_METATYPE_REVISION_HISTORY, line);
    793       free (line);
    794     }
    795   }
    796   free (author);
    797   free (date);
    798   free (userid);
    799 }
    800 
    801 
    802 /**
    803  * Scan a buffer of XML for tags and hand each of them to
    804  * #handle_tag().
    805  *
    806  * @param mc our extraction state
    807  * @param buf buffer to scan
    808  * @param len number of bytes in @a buf
    809  * @return number of bytes consumed; the remainder is an incomplete tag
    810  *         that the caller should present again with more data
    811  */
    812 static size_t
    813 scan_tags (struct MsoContext *mc,
    814            const char *buf,
    815            size_t len)
    816 {
    817   const char *p = buf;
    818   const char *end = buf + len;
    819 
    820   while (p < end)
    821   {
    822     const char *lt;
    823     const char *gt;
    824 
    825     if (NULL == (lt = memchr (p, '<', (size_t) (end - p))))
    826       return len;
    827     if (NULL == (gt = memchr (lt, '>', (size_t) (end - lt))))
    828       return (size_t) (lt - buf);
    829     handle_tag (mc, lt, (size_t) (gt - lt + 1));
    830     if (0 != mc->stop)
    831       return len;
    832     p = gt + 1;
    833   }
    834   return len;
    835 }
    836 
    837 
    838 /* ******************** reading ZIP parts ******************** */
    839 
    840 
    841 /**
    842  * Read the currently selected ZIP entry into a fresh 0-terminated
    843  * buffer.
    844  *
    845  * @param uf the ZIP archive
    846  * @param max maximum number of bytes to read
    847  * @param size set to the number of bytes read
    848  * @return the buffer to be freed by the caller, NULL on error
    849  */
    850 static char *
    851 read_current (struct EXTRACTOR_UnzipFile *uf,
    852               size_t max,
    853               size_t *size)
    854 {
    855   struct EXTRACTOR_UnzipFileInfo fi;
    856   char *buf;
    857   ssize_t got;
    858   size_t want;
    859 
    860   if (EXTRACTOR_UNZIP_OK !=
    861       EXTRACTOR_common_unzip_get_current_file_info (uf, &fi, NULL, 0,
    862                                                     NULL, 0, NULL, 0))
    863     return NULL;
    864   want = fi.uncompressed_size;
    865   if (want > max)
    866     want = max;
    867   if (0 == want)
    868     return NULL;
    869   if (EXTRACTOR_UNZIP_OK !=
    870       EXTRACTOR_common_unzip_open_current_file (uf))
    871     return NULL;
    872   if (NULL == (buf = malloc (want + 1)))
    873   {
    874     EXTRACTOR_common_unzip_close_current_file (uf);
    875     return NULL;
    876   }
    877   got = EXTRACTOR_common_unzip_read_current_file (uf, buf, want);
    878   EXTRACTOR_common_unzip_close_current_file (uf);
    879   if (0 >= got)
    880   {
    881     free (buf);
    882     return NULL;
    883   }
    884   buf[got] = '\0';
    885   *size = (size_t) got;
    886   return buf;
    887 }
    888 
    889 
    890 /**
    891  * Read the part @a name into memory and return it 0-terminated.
    892  *
    893  * @param uf the ZIP archive
    894  * @param name name of the part
    895  * @return the buffer to be freed by the caller, NULL if the part is
    896  *         absent, empty or too large
    897  */
    898 static char *
    899 read_part (struct EXTRACTOR_UnzipFile *uf,
    900            const char *name)
    901 {
    902   size_t size;
    903 
    904   if (EXTRACTOR_UNZIP_OK !=
    905       EXTRACTOR_common_unzip_go_find_local_file (uf, name, 2))
    906     return NULL;
    907   return read_current (uf, MAX_SMALL_PART, &size);
    908 }
    909 
    910 
    911 /**
    912  * Scan the part @a name for tags describing who edited the document.
    913  * The part is read through a sliding window, so that a part far too
    914  * large to hold in memory (`word/document.xml' of a heavily revised
    915  * document, or a decompression bomb) costs us a bounded amount of
    916  * memory.
    917  *
    918  * @param mc our extraction state
    919  * @param uf the ZIP archive
    920  * @param name name of the part to scan
    921  */
    922 static void
    923 scan_part (struct MsoContext *mc,
    924            struct EXTRACTOR_UnzipFile *uf,
    925            const char *name)
    926 {
    927   char *buf;
    928   size_t carry = 0;
    929   uint64_t total = 0;
    930 
    931   if (EXTRACTOR_UNZIP_OK !=
    932       EXTRACTOR_common_unzip_go_find_local_file (uf, name, 2))
    933     return;
    934   if (EXTRACTOR_UNZIP_OK !=
    935       EXTRACTOR_common_unzip_open_current_file (uf))
    936     return;
    937   /* One byte of slack so that the window is always 0-terminated: the
    938      attribute values we hand to xml_unescape() point into it. */
    939   if (NULL == (buf = malloc (SCAN_CHUNK + 1)))
    940   {
    941     EXTRACTOR_common_unzip_close_current_file (uf);
    942     return;
    943   }
    944   while (0 == mc->stop)
    945   {
    946     ssize_t got;
    947     size_t have;
    948     size_t used;
    949 
    950     got = EXTRACTOR_common_unzip_read_current_file (uf,
    951                                                     &buf[carry],
    952                                                     SCAN_CHUNK - carry);
    953     if (0 >= got)
    954       break;
    955     have = carry + (size_t) got;
    956     total += (uint64_t) got;
    957     buf[have] = '\0';
    958     used = scan_tags (mc, buf, have);
    959     carry = have - used;
    960     if (MAX_TAG < carry)
    961       carry = 0;   /* absurdly long tag; resynchronise */
    962     if (0 != carry)
    963       memmove (buf, &buf[used], carry);
    964     if (MAX_SCAN_BYTES < total)
    965       break;
    966   }
    967   free (buf);
    968   EXTRACTOR_common_unzip_close_current_file (uf);
    969 }
    970 
    971 
    972 /**
    973  * Does @a name start with @a prefix?
    974  *
    975  * @param name string to test
    976  * @param prefix prefix to test for
    977  * @return 1 on match, 0 otherwise
    978  */
    979 static int
    980 has_prefix (const char *name,
    981             const char *prefix)
    982 {
    983   return 0 == strncmp (name, prefix, strlen (prefix));
    984 }
    985 
    986 
    987 /**
    988  * Does @a name end with @a suffix?
    989  *
    990  * @param name string to test
    991  * @param suffix suffix to test for
    992  * @return 1 on match, 0 otherwise
    993  */
    994 static int
    995 has_suffix (const char *name,
    996             const char *suffix)
    997 {
    998   size_t nl = strlen (name);
    999   size_t sl = strlen (suffix);
   1000 
   1001   return (nl >= sl) && (0 == strcmp (&name[nl - sl], suffix));
   1002 }
   1003 
   1004 
   1005 /**
   1006  * Is @a name a part that can name the people who worked on the
   1007  * document?
   1008  *
   1009  * @param name name of a part inside the OOXML package
   1010  * @return 1 if the part is worth scanning, 0 otherwise
   1011  */
   1012 static int
   1013 is_people_part (const char *name)
   1014 {
   1015   if (! has_suffix (name, ".xml"))
   1016     return 0;
   1017   /* Word: tracked changes live in the document body and in every
   1018      story around it; comments and the identities behind them live in
   1019      parts of their own. */
   1020   if ( (0 == strcmp (name, "word/document.xml")) ||
   1021        (0 == strcmp (name, "word/footnotes.xml")) ||
   1022        (0 == strcmp (name, "word/endnotes.xml")) ||
   1023        (0 == strcmp (name, "word/comments.xml")) ||
   1024        (0 == strcmp (name, "word/commentsExtended.xml")) ||
   1025        (0 == strcmp (name, "word/people.xml")) ||
   1026        (has_prefix (name, "word/header")) ||
   1027        (has_prefix (name, "word/footer")) )
   1028     return 1;
   1029   /* Excel: the workbook part carries <fileSharing userName="...">,
   1030      the direct descendant of the BIFF FILESHARING record. */
   1031   if (0 == strcmp (name, "xl/workbook.xml"))
   1032     return 1;
   1033   /* Excel: the shared workbook revision log, the modern threaded
   1034      comment authors and the classic comment author list. */
   1035   if ( (has_prefix (name, "xl/revisions/")) ||
   1036        (has_prefix (name, "xl/persons/")) ||
   1037        (has_prefix (name, "xl/threadedComments/")) ||
   1038        (has_prefix (name, "xl/comments")) )
   1039     return 1;
   1040   /* PowerPoint: the comment author list and the comments themselves. */
   1041   if ( (0 == strcmp (name, "ppt/commentAuthors.xml")) ||
   1042        (0 == strcmp (name, "ppt/authors.xml")) ||
   1043        (has_prefix (name, "ppt/comments/")) )
   1044     return 1;
   1045   return 0;
   1046 }
   1047 
   1048 
   1049 /* ******************** the two formats ******************** */
   1050 
   1051 
   1052 /**
   1053  * Extract meta data from an Office Open XML package.
   1054  *
   1055  * @param mc our extraction state
   1056  * @return 1 if the file was an OOXML package, 0 if not
   1057  */
   1058 static int
   1059 extract_ooxml (struct MsoContext *mc)
   1060 {
   1061   struct EXTRACTOR_UnzipFile *uf;
   1062   char *parts[MAX_PARTS];
   1063   unsigned int num_parts = 0;
   1064   unsigned int i;
   1065   const char *mime = NULL;
   1066   char *buf;
   1067   int ret;
   1068 
   1069   if (NULL == (uf = EXTRACTOR_common_unzip_open (mc->ec)))
   1070     return 0;
   1071   /* First pass: learn what kind of document this is and which parts
   1072      are worth looking at.  We collect the names instead of extracting
   1073      as we iterate, so that seeking around the archive cannot disturb
   1074      the walk over the central directory. */
   1075   ret = EXTRACTOR_common_unzip_go_to_first_file (uf);
   1076   while (EXTRACTOR_UNZIP_OK == ret)
   1077   {
   1078     char name[MAXFILENAME];
   1079 
   1080     if (EXTRACTOR_UNZIP_OK ==
   1081         EXTRACTOR_common_unzip_get_current_file_info (uf, NULL,
   1082                                                       name, sizeof (name),
   1083                                                       NULL, 0, NULL, 0))
   1084     {
   1085       name[sizeof (name) - 1] = '\0';
   1086       if (0 == strcmp (name, "word/document.xml"))
   1087         mime =
   1088           "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
   1089       else if (0 == strcmp (name, "xl/workbook.xml"))
   1090         mime =
   1091           "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
   1092       else if (0 == strcmp (name, "ppt/presentation.xml"))
   1093         mime =
   1094           "application/vnd.openxmlformats-officedocument.presentationml.presentation";
   1095       if ( (is_people_part (name)) &&
   1096            (MAX_PARTS > num_parts) )
   1097       {
   1098         char *dup = strdup (name);
   1099 
   1100         if (NULL != dup)
   1101           parts[num_parts++] = dup;
   1102       }
   1103     }
   1104     ret = EXTRACTOR_common_unzip_go_to_next_file (uf);
   1105   }
   1106   if (NULL == mime)
   1107   {
   1108     /* Not an OOXML document -- most likely an ODF file or a plain ZIP,
   1109        both of which have plugins of their own. */
   1110     for (i = 0; i < num_parts; i++)
   1111       free (parts[i]);
   1112     EXTRACTOR_common_unzip_close (uf);
   1113     return 0;
   1114   }
   1115   add_meta (mc, EXTRACTOR_METATYPE_MIMETYPE, mime);
   1116 
   1117   if (NULL != (buf = read_part (uf, "docProps/core.xml")))
   1118   {
   1119     for (i = 0; NULL != core_map[i].text; i++)
   1120       report_element (mc, buf, core_map[i].text, core_map[i].type);
   1121     free (buf);
   1122   }
   1123   if (NULL != (buf = read_part (uf, "docProps/app.xml")))
   1124   {
   1125     for (i = 0; NULL != app_map[i].text; i++)
   1126       report_element (mc, buf, app_map[i].text, app_map[i].type);
   1127     free (buf);
   1128   }
   1129   if (NULL != (buf = read_part (uf, "docProps/custom.xml")))
   1130   {
   1131     for (i = 0; NULL != custom_map[i].text; i++)
   1132       report_custom_property (mc, buf, custom_map[i].text, custom_map[i].type);
   1133     free (buf);
   1134   }
   1135   for (i = 0; (i < num_parts) && (0 == mc->stop); i++)
   1136   {
   1137     /* Excel keeps the authors of classic cell comments in a list of
   1138        <author> elements rather than in an attribute. */
   1139     if (has_prefix (parts[i], "xl/comments"))
   1140     {
   1141       if (NULL != (buf = read_part (uf, parts[i])))
   1142       {
   1143         report_element (mc, buf, "author",
   1144                         EXTRACTOR_METATYPE_CONTRIBUTOR_NAME);
   1145         free (buf);
   1146       }
   1147     }
   1148     scan_part (mc, uf, parts[i]);
   1149   }
   1150   for (i = 0; i < num_parts; i++)
   1151     free (parts[i]);
   1152   EXTRACTOR_common_unzip_close (uf);
   1153   return 1;
   1154 }
   1155 
   1156 
   1157 /**
   1158  * Extract meta data from a bare BIFF stream, as written by Excel 2 to
   1159  * Excel 4 (and still produced by some exporters).
   1160  *
   1161  * @param mc our extraction state
   1162  * @return 1 if the file was a BIFF stream, 0 if not
   1163  */
   1164 static int
   1165 extract_biff (struct MsoContext *mc)
   1166 {
   1167   struct EXTRACTOR_ExtractContext *ec = mc->ec;
   1168   void *data;
   1169   ssize_t avail;
   1170   uint64_t fsize;
   1171 
   1172   fsize = ec->get_size (ec->cls);
   1173   if ( (8 > fsize) ||
   1174        (0 != ec->seek (ec->cls, 0, SEEK_SET)) )
   1175     return 0;
   1176   /* The File Protection Block is the very first thing in the stream,
   1177      so a small window is enough. */
   1178   avail = ec->read (ec->cls,
   1179                     &data,
   1180                     (fsize < 64 * 1024) ? (size_t) fsize : 64 * 1024);
   1181   if (8 > avail)
   1182     return 0;
   1183   if (0 > EXTRACTOR_msoffice_biff_extract ((const unsigned char *) data,
   1184                                            (size_t) avail,
   1185                                            PLUGIN_NAME,
   1186                                            ec->proc,
   1187                                            ec->cls))
   1188     return 0;
   1189   add_meta (mc, EXTRACTOR_METATYPE_MIMETYPE, "application/vnd.ms-excel");
   1190   return 1;
   1191 }
   1192 
   1193 
   1194 /**
   1195  * Main entry method for the MS Office extraction plugin.
   1196  *
   1197  * @param ec extraction context provided to the plugin
   1198  */
   1199 void
   1200 EXTRACTOR_msoffice_extract_method (struct EXTRACTOR_ExtractContext *ec);
   1201 
   1202 
   1203 void
   1204 EXTRACTOR_msoffice_extract_method (struct EXTRACTOR_ExtractContext *ec)
   1205 {
   1206   struct MsoContext mc;
   1207   unsigned int i;
   1208   void *data;
   1209   ssize_t avail;
   1210 
   1211   memset (&mc, 0, sizeof (mc));
   1212   mc.ec = ec;
   1213   if (0 != ec->seek (ec->cls, 0, SEEK_SET))
   1214     return;
   1215   if (4 > (avail = ec->read (ec->cls, &data, 4)))
   1216     return;
   1217   if (0 == memcmp (data, "PK\003\004", 4))
   1218     extract_ooxml (&mc);
   1219   else
   1220     extract_biff (&mc);
   1221   for (i = 0; i < mc.seen_len; i++)
   1222     free (mc.seen[i]);
   1223 }
   1224 
   1225 
   1226 /* end of msoffice_extractor.c */