libextractor

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

msoffice_biff.h (12145B)


      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_biff.h
     22  * @brief extract user names from the "File Protection Block" of an
     23  *        Excel BIFF stream
     24  * @author Christian Grothoff
     25  *
     26  * Excel stores the name of the user that saved the file in the
     27  * WRITEACCESS record, and the name of the user that write-protected
     28  * the file in the FILESHARING record.  Both are part of the "File
     29  * Protection Block" at the very beginning of the workbook globals
     30  * substream (section 4.19 of OpenOffice.org's documentation of the
     31  * Microsoft Excel file format, [MS-XLS] sections 2.4.349 and 2.4.117).
     32  * Neither is covered by the OLE2 property sets, so this information is
     33  * missed by extractors that only look at (Document)SummaryInformation.
     34  *
     35  * The very same substream exists both inside the "Workbook" (BIFF8) or
     36  * "Book" (BIFF5) stream of an OLE2 container *and* as the entire
     37  * content of a pre-OLE2 Excel file (BIFF2-BIFF4 wrote the record
     38  * stream straight to disk).  This header is therefore shared between
     39  * the `ole2' plugin (which has an OLE2 container to open first) and
     40  * the `msoffice' plugin (which handles the bare streams).
     41  */
     42 #ifndef MSOFFICE_BIFF_H
     43 #define MSOFFICE_BIFF_H
     44 
     45 #include "platform.h"
     46 #include "extractor.h"
     47 #include "convert.h"
     48 
     49 
     50 /**
     51  * BIFF record identifier for FILESHARING.
     52  */
     53 #define BIFF_ID_FILESHARING 0x005B
     54 
     55 /**
     56  * BIFF record identifier for WRITEACCESS.
     57  */
     58 #define BIFF_ID_WRITEACCESS 0x005C
     59 
     60 /**
     61  * BIFF record identifier for CODEPAGE.
     62  */
     63 #define BIFF_ID_CODEPAGE 0x0042
     64 
     65 /**
     66  * BIFF record identifier for EOF.
     67  */
     68 #define BIFF_ID_EOF 0x000A
     69 
     70 /**
     71  * Longest user name we are willing to report.  The format allows for
     72  * 109 characters; we accept a bit more to tolerate sloppy writers, but
     73  * refuse to allocate unbounded amounts of memory.
     74  */
     75 #define BIFF_MAX_NAME_LEN 1024
     76 
     77 /**
     78  * Maximum number of records we inspect before giving up.  The File
     79  * Protection Block is at the very start of the globals substream, so
     80  * a small limit is plenty and bounds the work done on hostile input.
     81  */
     82 #define BIFF_MAX_RECORDS 256
     83 
     84 
     85 /**
     86  * Read a 16 bit little endian value.
     87  *
     88  * @param p buffer to read from, must have at least 2 valid bytes
     89  * @return the value read
     90  */
     91 static uint16_t
     92 biff_le16 (const unsigned char *p)
     93 {
     94   return (uint16_t) (p[0] | (p[1] << 8));
     95 }
     96 
     97 
     98 /**
     99  * Determine the BIFF version a stream is written in from its leading
    100  * BOF record.
    101  *
    102  * @param rid record identifier of the first record
    103  * @param data payload of the first record
    104  * @param size number of bytes in @a data
    105  * @return 2, 3, 4, 5 or 8, or 0 if @a rid is not a BOF record
    106  */
    107 static unsigned int
    108 biff_version_from_bof (uint16_t rid,
    109                        const unsigned char *data,
    110                        size_t size)
    111 {
    112   switch (rid)
    113   {
    114   case 0x0009:
    115     return 2;
    116   case 0x0209:
    117     return 3;
    118   case 0x0409:
    119     return 4;
    120   case 0x0809:
    121     /* BIFF5 and BIFF8 share the record identifier and are told apart
    122        by the version field of the record. */
    123     if (size < 2)
    124       return 0;
    125     return (biff_le16 (data) >= 0x0600) ? 8 : 5;
    126   default:
    127     return 0;
    128   }
    129 }
    130 
    131 
    132 /**
    133  * Map the value of a CODEPAGE record to an iconv character set name.
    134  *
    135  * @param cv value of the CODEPAGE record
    136  * @return name of the character set, NULL if unknown
    137  */
    138 static const char *
    139 biff_codepage_to_charset (uint16_t cv)
    140 {
    141   switch (cv)
    142   {
    143   case 367:
    144     return "ASCII";
    145   case 437:
    146     return "CP437";
    147   case 737:
    148     return "CP737";
    149   case 775:
    150     return "CP775";
    151   case 850:
    152     return "CP850";
    153   case 852:
    154     return "CP852";
    155   case 855:
    156     return "CP855";
    157   case 857:
    158     return "CP857";
    159   case 860:
    160     return "CP860";
    161   case 861:
    162     return "CP861";
    163   case 862:
    164     return "CP862";
    165   case 863:
    166     return "CP863";
    167   case 864:
    168     return "CP864";
    169   case 865:
    170     return "CP865";
    171   case 866:
    172     return "CP866";
    173   case 869:
    174     return "CP869";
    175   case 874:
    176     return "CP874";
    177   case 932:
    178     return "CP932";
    179   case 936:
    180     return "CP936";
    181   case 949:
    182     return "CP949";
    183   case 950:
    184     return "CP950";
    185   case 1200:
    186     return "UTF-16LE";
    187   case 1250:
    188     return "CP1250";
    189   case 1251:
    190     return "CP1251";
    191   case 32769: /* BIFF2-BIFF3 wrote this for "ANSI" */
    192   case 1252:
    193     return "CP1252";
    194   case 1253:
    195     return "CP1253";
    196   case 1254:
    197     return "CP1254";
    198   case 1255:
    199     return "CP1255";
    200   case 1256:
    201     return "CP1256";
    202   case 1257:
    203     return "CP1257";
    204   case 1258:
    205     return "CP1258";
    206   case 10000:
    207   case 32768:
    208     return "MACINTOSH";
    209   default:
    210     return NULL;
    211   }
    212 }
    213 
    214 
    215 /**
    216  * Strip trailing spaces (used as padding by WRITEACCESS) and leading
    217  * and trailing white space from @a s, in place.
    218  *
    219  * @param s 0-terminated string to trim
    220  * @return pointer into @a s to the first non-blank character
    221  */
    222 static char *
    223 biff_trim (char *s)
    224 {
    225   size_t len = strlen (s);
    226 
    227   while ( (0 < len) &&
    228           (isspace ((unsigned char) s[len - 1])) )
    229     s[--len] = '\0';
    230   while (isspace ((unsigned char) s[0]))
    231     s++;
    232   return s;
    233 }
    234 
    235 
    236 /**
    237  * Decode a BIFF string into UTF-8.
    238  *
    239  * For BIFF2-BIFF5 this is a "byte string with 8-bit string length"
    240  * (section 2.5.2): one length byte followed by that many characters in
    241  * the workbook's code page.  For BIFF8 it is a "Unicode string with
    242  * 16-bit string length" (section 2.5.3): a 16 bit character count, a
    243  * flags byte and then either 8 bit (compressed) or 16 bit characters,
    244  * possibly preceded by rich text and phonetic size fields.
    245  *
    246  * @param data record payload
    247  * @param size number of bytes in @a data
    248  * @param off offset of the string within @a data
    249  * @param biff BIFF version of the stream
    250  * @param charset character set to assume for 8 bit characters
    251  * @return UTF-8 string to be freed by the caller, NULL on error
    252  */
    253 static char *
    254 biff_decode_string (const unsigned char *data,
    255                     size_t size,
    256                     size_t off,
    257                     unsigned int biff,
    258                     const char *charset)
    259 {
    260   size_t cch;
    261   size_t need;
    262 
    263   if (off >= size)
    264     return NULL;
    265   if (8 > biff)
    266   {
    267     cch = data[off++];
    268     if ( (0 == cch) ||
    269          (BIFF_MAX_NAME_LEN < cch) ||
    270          (off + cch > size) )
    271       return NULL;
    272     return EXTRACTOR_common_convert_to_utf8 ((const char *) &data[off],
    273                                              cch,
    274                                              charset);
    275   }
    276   if (off + 3 > size)
    277     return NULL;
    278   cch = biff_le16 (&data[off]);
    279   {
    280     unsigned char flags = data[off + 2];
    281 
    282     off += 3;
    283     if (0 != (flags & 0x08))
    284       off += 2;    /* cRun of a rich text string */
    285     if (0 != (flags & 0x04))
    286       off += 4;    /* cbExtRst of an Asian phonetic string */
    287     if ( (0 == cch) ||
    288          (BIFF_MAX_NAME_LEN < cch) )
    289       return NULL;
    290     need = (0 != (flags & 0x01)) ? 2 * cch : cch;
    291     if ( (off > size) ||
    292          (off + need > size) )
    293       return NULL;
    294     return EXTRACTOR_common_convert_to_utf8 ((const char *) &data[off],
    295                                              need,
    296                                              (0 != (flags & 0x01))
    297                                              ? "UTF-16LE"
    298                                              : charset);
    299   }
    300 }
    301 
    302 
    303 /**
    304  * Determine the character set of a BIFF stream by locating its
    305  * CODEPAGE record.  The File Protection Block precedes CODEPAGE in the
    306  * stream, so we have to look ahead before we can decode any of its
    307  * strings.
    308  *
    309  * @param data the BIFF stream
    310  * @param size number of bytes in @a data
    311  * @return name of the character set, never NULL
    312  */
    313 static const char *
    314 biff_find_charset (const unsigned char *data,
    315                    size_t size)
    316 {
    317   size_t off = 0;
    318   unsigned int i;
    319 
    320   for (i = 0; i < BIFF_MAX_RECORDS; i++)
    321   {
    322     uint16_t rid;
    323     uint16_t len;
    324 
    325     if (off + 4 > size)
    326       break;
    327     rid = biff_le16 (&data[off]);
    328     len = biff_le16 (&data[off + 2]);
    329     if (off + 4 + (size_t) len > size)
    330       break;
    331     if (BIFF_ID_CODEPAGE == rid)
    332     {
    333       const char *cs;
    334 
    335       if ( (2 <= len) &&
    336            (NULL != (cs = biff_codepage_to_charset (biff_le16 (&data[off
    337                                                                      + 4])))) )
    338         return cs;
    339       break;
    340     }
    341     if ( (BIFF_ID_EOF == rid) &&
    342          (0 < i) )
    343       break;
    344     off += 4 + (size_t) len;
    345   }
    346   return "CP1252";
    347 }
    348 
    349 
    350 /**
    351  * Scan the globals substream of a BIFF stream and report the user
    352  * names found in its File Protection Block.
    353  *
    354  * @param data the BIFF stream, starting at its BOF record
    355  * @param size number of bytes in @a data
    356  * @param plugin_name name to report to @a proc as the source plugin
    357  * @param proc function to call on meta data found
    358  * @param proc_cls closure for @a proc
    359  * @return 0 to continue extracting, 1 if @a proc asked us to stop,
    360  *         -1 if @a data does not start with a BOF record
    361  */
    362 static int
    363 EXTRACTOR_msoffice_biff_extract (const unsigned char *data,
    364                                  size_t size,
    365                                  const char *plugin_name,
    366                                  EXTRACTOR_MetaDataProcessor proc,
    367                                  void *proc_cls)
    368 {
    369   const char *charset;
    370   size_t off = 0;
    371   unsigned int biff;
    372   unsigned int i;
    373   int ret = 0;
    374 
    375   if (4 > size)
    376     return -1;
    377   biff = biff_version_from_bof (biff_le16 (data),
    378                                 &data[4],
    379                                 (size - 4 < (size_t) biff_le16 (&data[2]))
    380                                 ? size - 4
    381                                 : (size_t) biff_le16 (&data[2]));
    382   if (0 == biff)
    383     return -1;
    384   charset = (8 == biff) ? "CP1252" : biff_find_charset (data, size);
    385   for (i = 0; i < BIFF_MAX_RECORDS; i++)
    386   {
    387     uint16_t rid;
    388     uint16_t len;
    389     const unsigned char *rec;
    390     char *name = NULL;
    391     enum EXTRACTOR_MetaType type;
    392 
    393     if (off + 4 > size)
    394       break;
    395     rid = biff_le16 (&data[off]);
    396     len = biff_le16 (&data[off + 2]);
    397     if (off + 4 + (size_t) len > size)
    398       break;
    399     rec = &data[off + 4];
    400     off += 4 + (size_t) len;
    401     if ( (BIFF_ID_EOF == rid) &&
    402          (0 < i) )
    403       break;   /* end of the globals substream */
    404     switch (rid)
    405     {
    406     case BIFF_ID_WRITEACCESS:
    407       /* The user name that saved the file.  Note that the record is
    408          padded with spaces to a fixed size, and that the size differs
    409          between BIFF versions (and between writers). */
    410       name = biff_decode_string (rec, len, 0, biff, charset);
    411       type = EXTRACTOR_METATYPE_LAST_SAVED_BY;
    412       break;
    413     case BIFF_ID_FILESHARING:
    414       /* The user name that write-protected the file, preceded by the
    415          read-only recommendation flag and the password hash. */
    416       if (5 > len)
    417         break;
    418       name = biff_decode_string (rec, len, 4, biff, charset);
    419       type = EXTRACTOR_METATYPE_CREATOR;
    420       break;
    421     default:
    422       break;
    423     }
    424     if (NULL == name)
    425       continue;
    426     {
    427       char *trimmed = biff_trim (name);
    428 
    429       if ( ('\0' != trimmed[0]) &&
    430            (0 != proc (proc_cls,
    431                        plugin_name,
    432                        type,
    433                        EXTRACTOR_METAFORMAT_UTF8,
    434                        "text/plain",
    435                        trimmed,
    436                        strlen (trimmed) + 1)) )
    437         ret = 1;
    438     }
    439     free (name);
    440     if (0 != ret)
    441       break;
    442   }
    443   return ret;
    444 }
    445 
    446 
    447 #endif
    448 /* end of msoffice_biff.h */