/*
     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_biff.h
 * @brief extract user names from the "File Protection Block" of an
 *        Excel BIFF stream
 * @author Christian Grothoff
 *
 * Excel stores the name of the user that saved the file in the
 * WRITEACCESS record, and the name of the user that write-protected
 * the file in the FILESHARING record.  Both are part of the "File
 * Protection Block" at the very beginning of the workbook globals
 * substream (section 4.19 of OpenOffice.org's documentation of the
 * Microsoft Excel file format, [MS-XLS] sections 2.4.349 and 2.4.117).
 * Neither is covered by the OLE2 property sets, so this information is
 * missed by extractors that only look at (Document)SummaryInformation.
 *
 * The very same substream exists both inside the "Workbook" (BIFF8) or
 * "Book" (BIFF5) stream of an OLE2 container *and* as the entire
 * content of a pre-OLE2 Excel file (BIFF2-BIFF4 wrote the record
 * stream straight to disk).  This header is therefore shared between
 * the `ole2' plugin (which has an OLE2 container to open first) and
 * the `msoffice' plugin (which handles the bare streams).
 */
#ifndef MSOFFICE_BIFF_H
#define MSOFFICE_BIFF_H

#include "platform.h"
#include "extractor.h"
#include "convert.h"


/**
 * BIFF record identifier for FILESHARING.
 */
#define BIFF_ID_FILESHARING 0x005B

/**
 * BIFF record identifier for WRITEACCESS.
 */
#define BIFF_ID_WRITEACCESS 0x005C

/**
 * BIFF record identifier for CODEPAGE.
 */
#define BIFF_ID_CODEPAGE 0x0042

/**
 * BIFF record identifier for EOF.
 */
#define BIFF_ID_EOF 0x000A

/**
 * Longest user name we are willing to report.  The format allows for
 * 109 characters; we accept a bit more to tolerate sloppy writers, but
 * refuse to allocate unbounded amounts of memory.
 */
#define BIFF_MAX_NAME_LEN 1024

/**
 * Maximum number of records we inspect before giving up.  The File
 * Protection Block is at the very start of the globals substream, so
 * a small limit is plenty and bounds the work done on hostile input.
 */
#define BIFF_MAX_RECORDS 256


/**
 * Read a 16 bit little endian value.
 *
 * @param p buffer to read from, must have at least 2 valid bytes
 * @return the value read
 */
static uint16_t
biff_le16 (const unsigned char *p)
{
  return (uint16_t) (p[0] | (p[1] << 8));
}


/**
 * Determine the BIFF version a stream is written in from its leading
 * BOF record.
 *
 * @param rid record identifier of the first record
 * @param data payload of the first record
 * @param size number of bytes in @a data
 * @return 2, 3, 4, 5 or 8, or 0 if @a rid is not a BOF record
 */
static unsigned int
biff_version_from_bof (uint16_t rid,
                       const unsigned char *data,
                       size_t size)
{
  switch (rid)
  {
  case 0x0009:
    return 2;
  case 0x0209:
    return 3;
  case 0x0409:
    return 4;
  case 0x0809:
    /* BIFF5 and BIFF8 share the record identifier and are told apart
       by the version field of the record. */
    if (size < 2)
      return 0;
    return (biff_le16 (data) >= 0x0600) ? 8 : 5;
  default:
    return 0;
  }
}


/**
 * Map the value of a CODEPAGE record to an iconv character set name.
 *
 * @param cv value of the CODEPAGE record
 * @return name of the character set, NULL if unknown
 */
static const char *
biff_codepage_to_charset (uint16_t cv)
{
  switch (cv)
  {
  case 367:
    return "ASCII";
  case 437:
    return "CP437";
  case 737:
    return "CP737";
  case 775:
    return "CP775";
  case 850:
    return "CP850";
  case 852:
    return "CP852";
  case 855:
    return "CP855";
  case 857:
    return "CP857";
  case 860:
    return "CP860";
  case 861:
    return "CP861";
  case 862:
    return "CP862";
  case 863:
    return "CP863";
  case 864:
    return "CP864";
  case 865:
    return "CP865";
  case 866:
    return "CP866";
  case 869:
    return "CP869";
  case 874:
    return "CP874";
  case 932:
    return "CP932";
  case 936:
    return "CP936";
  case 949:
    return "CP949";
  case 950:
    return "CP950";
  case 1200:
    return "UTF-16LE";
  case 1250:
    return "CP1250";
  case 1251:
    return "CP1251";
  case 32769: /* BIFF2-BIFF3 wrote this for "ANSI" */
  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:
  case 32768:
    return "MACINTOSH";
  default:
    return NULL;
  }
}


/**
 * Strip trailing spaces (used as padding by WRITEACCESS) and leading
 * and trailing white space from @a s, in place.
 *
 * @param s 0-terminated string to trim
 * @return pointer into @a s to the first non-blank character
 */
static char *
biff_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;
}


/**
 * Decode a BIFF string into UTF-8.
 *
 * For BIFF2-BIFF5 this is a "byte string with 8-bit string length"
 * (section 2.5.2): one length byte followed by that many characters in
 * the workbook's code page.  For BIFF8 it is a "Unicode string with
 * 16-bit string length" (section 2.5.3): a 16 bit character count, a
 * flags byte and then either 8 bit (compressed) or 16 bit characters,
 * possibly preceded by rich text and phonetic size fields.
 *
 * @param data record payload
 * @param size number of bytes in @a data
 * @param off offset of the string within @a data
 * @param biff BIFF version of the stream
 * @param charset character set to assume for 8 bit characters
 * @return UTF-8 string to be freed by the caller, NULL on error
 */
static char *
biff_decode_string (const unsigned char *data,
                    size_t size,
                    size_t off,
                    unsigned int biff,
                    const char *charset)
{
  size_t cch;
  size_t need;

  if (off >= size)
    return NULL;
  if (8 > biff)
  {
    cch = data[off++];
    if ( (0 == cch) ||
         (BIFF_MAX_NAME_LEN < cch) ||
         (off + cch > size) )
      return NULL;
    return EXTRACTOR_common_convert_to_utf8 ((const char *) &data[off],
                                             cch,
                                             charset);
  }
  if (off + 3 > size)
    return NULL;
  cch = biff_le16 (&data[off]);
  {
    unsigned char flags = data[off + 2];

    off += 3;
    if (0 != (flags & 0x08))
      off += 2;    /* cRun of a rich text string */
    if (0 != (flags & 0x04))
      off += 4;    /* cbExtRst of an Asian phonetic string */
    if ( (0 == cch) ||
         (BIFF_MAX_NAME_LEN < cch) )
      return NULL;
    need = (0 != (flags & 0x01)) ? 2 * cch : cch;
    if ( (off > size) ||
         (off + need > size) )
      return NULL;
    return EXTRACTOR_common_convert_to_utf8 ((const char *) &data[off],
                                             need,
                                             (0 != (flags & 0x01))
                                             ? "UTF-16LE"
                                             : charset);
  }
}


/**
 * Determine the character set of a BIFF stream by locating its
 * CODEPAGE record.  The File Protection Block precedes CODEPAGE in the
 * stream, so we have to look ahead before we can decode any of its
 * strings.
 *
 * @param data the BIFF stream
 * @param size number of bytes in @a data
 * @return name of the character set, never NULL
 */
static const char *
biff_find_charset (const unsigned char *data,
                   size_t size)
{
  size_t off = 0;
  unsigned int i;

  for (i = 0; i < BIFF_MAX_RECORDS; i++)
  {
    uint16_t rid;
    uint16_t len;

    if (off + 4 > size)
      break;
    rid = biff_le16 (&data[off]);
    len = biff_le16 (&data[off + 2]);
    if (off + 4 + (size_t) len > size)
      break;
    if (BIFF_ID_CODEPAGE == rid)
    {
      const char *cs;

      if ( (2 <= len) &&
           (NULL != (cs = biff_codepage_to_charset (biff_le16 (&data[off
                                                                     + 4])))) )
        return cs;
      break;
    }
    if ( (BIFF_ID_EOF == rid) &&
         (0 < i) )
      break;
    off += 4 + (size_t) len;
  }
  return "CP1252";
}


/**
 * Scan the globals substream of a BIFF stream and report the user
 * names found in its File Protection Block.
 *
 * @param data the BIFF stream, starting at its BOF record
 * @param size number of bytes in @a data
 * @param plugin_name name to report to @a proc as the source plugin
 * @param proc function to call on meta data found
 * @param proc_cls closure for @a proc
 * @return 0 to continue extracting, 1 if @a proc asked us to stop,
 *         -1 if @a data does not start with a BOF record
 */
static int
EXTRACTOR_msoffice_biff_extract (const unsigned char *data,
                                 size_t size,
                                 const char *plugin_name,
                                 EXTRACTOR_MetaDataProcessor proc,
                                 void *proc_cls)
{
  const char *charset;
  size_t off = 0;
  unsigned int biff;
  unsigned int i;
  int ret = 0;

  if (4 > size)
    return -1;
  biff = biff_version_from_bof (biff_le16 (data),
                                &data[4],
                                (size - 4 < (size_t) biff_le16 (&data[2]))
                                ? size - 4
                                : (size_t) biff_le16 (&data[2]));
  if (0 == biff)
    return -1;
  charset = (8 == biff) ? "CP1252" : biff_find_charset (data, size);
  for (i = 0; i < BIFF_MAX_RECORDS; i++)
  {
    uint16_t rid;
    uint16_t len;
    const unsigned char *rec;
    char *name = NULL;
    enum EXTRACTOR_MetaType type;

    if (off + 4 > size)
      break;
    rid = biff_le16 (&data[off]);
    len = biff_le16 (&data[off + 2]);
    if (off + 4 + (size_t) len > size)
      break;
    rec = &data[off + 4];
    off += 4 + (size_t) len;
    if ( (BIFF_ID_EOF == rid) &&
         (0 < i) )
      break;   /* end of the globals substream */
    switch (rid)
    {
    case BIFF_ID_WRITEACCESS:
      /* The user name that saved the file.  Note that the record is
         padded with spaces to a fixed size, and that the size differs
         between BIFF versions (and between writers). */
      name = biff_decode_string (rec, len, 0, biff, charset);
      type = EXTRACTOR_METATYPE_LAST_SAVED_BY;
      break;
    case BIFF_ID_FILESHARING:
      /* The user name that write-protected the file, preceded by the
         read-only recommendation flag and the password hash. */
      if (5 > len)
        break;
      name = biff_decode_string (rec, len, 4, biff, charset);
      type = EXTRACTOR_METATYPE_CREATOR;
      break;
    default:
      break;
    }
    if (NULL == name)
      continue;
    {
      char *trimmed = biff_trim (name);

      if ( ('\0' != trimmed[0]) &&
           (0 != proc (proc_cls,
                       plugin_name,
                       type,
                       EXTRACTOR_METAFORMAT_UTF8,
                       "text/plain",
                       trimmed,
                       strlen (trimmed) + 1)) )
        ret = 1;
    }
    free (name);
    if (0 != ret)
      break;
  }
  return ret;
}


#endif
/* end of msoffice_biff.h */
