libextractor

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

ebook_extractor.c (90401B)


      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/ebook_extractor.c
     22  * @brief plugin to support EPUB and MOBI/AZW electronic books
     23  * @author Christian Grothoff
     24  *
     25  * Two unrelated containers share this plugin because they answer the
     26  * same question.  An EPUB is a ZIP whose OPF package document carries
     27  * Dublin Core; a MOBI is a Palm database whose EXTH header carries the
     28  * same fields plus, on files that came from a store, a watermark.  What
     29  * is worth having here is not the title but the provenance: which tool
     30  * built the file, when, for whom.
     31  *
     32  * With the "fulltext" option the book's text is reported as one
     33  * #EXTRACTOR_METATYPE_FULL_TEXT item:
     34  * - EPUB: the linear entries of the OPF spine, in spine order (not
     35  *   manifest or archive order); each (X)HTML content document is
     36  *   converted by a tolerant markup scanner that drops head, script,
     37  *   style, nav, aside, page-level header/footer, hidden elements, ARIA
     38  *   landmark roles for the page margins and page break markers
     39  *   (epub:type="pagebreak"); block elements, <br><br> and chapter
     40  *   boundaries end paragraphs.  Documents listed in encryption.xml
     41  *   (DRM) are skipped.
     42  * - MOBI/AZW: the text records, uncompressed or PalmDOC compressed,
     43  *   with the trailing entries stripped, run through the same scanner;
     44  *   <mbp:pagebreak/> ends a paragraph.  Encrypted books and HUFF/CDIC
     45  *   compression (used by some Amazon-built files) are not supported.
     46  *   For KF8 (AZW3) the raw markup is scanned in storage order, which
     47  *   is the reading order of the text.
     48  *
     49  * References:
     50  * - EPUB 3 OCF and Package Document, https://www.w3.org/TR/epub-33/
     51  * - MOBI format, https://wiki.mobileread.com/wiki/MOBI
     52  * - PalmDOC / PDB, https://wiki.mobileread.com/wiki/PDB
     53  */
     54 #include "platform.h"
     55 #include "extractor.h"
     56 #include "forensics.h"
     57 #include "fulltext.h"
     58 #include "unzip.h"
     59 
     60 
     61 /**
     62  * Name we report our meta data under.
     63  */
     64 #define PLUGIN_NAME "ebook"
     65 
     66 /**
     67  * Never read more than this from `META-INF/container.xml'.  It holds a
     68  * handful of paths; anything larger is padding.
     69  */
     70 #define MAX_CONTAINER (64 * 1024)
     71 
     72 /**
     73  * Never read more than this from the OPF package document.  Real ones
     74  * are a few kilobytes plus the manifest.
     75  */
     76 #define MAX_OPF (256 * 1024)
     77 
     78 /**
     79  * Never read more than this from record 0 of a Palm database.  The MOBI
     80  * and EXTH headers together are a few kilobytes.
     81  */
     82 #define MAX_RECORD0 (64 * 1024)
     83 
     84 /**
     85  * Upper bound on the number of EXTH records we will walk.
     86  */
     87 #define MAX_EXTH 1024
     88 
     89 /**
     90  * Upper bound on the number of XML elements we will walk in the OPF.
     91  */
     92 #define MAX_ELEMENTS 8192
     93 
     94 /**
     95  * Seconds between the Palm epoch (1904-01-01) and the Unix epoch.
     96  */
     97 #define PALM_EPOCH_OFFSET 2082844800LL
     98 
     99 
    100 /**
    101  * Find @a needle in @a hay.  `memmem()' is a GNU extension, and this is
    102  * only ever run over buffers of a few hundred kilobytes.
    103  *
    104  * @param hay buffer to search
    105  * @param hlen number of bytes in @a hay
    106  * @param needle bytes to look for
    107  * @param nlen number of bytes in @a needle
    108  * @return pointer into @a hay, NULL if @a needle does not occur
    109  */
    110 static const char *
    111 mem_find (const char *hay,
    112           size_t hlen,
    113           const char *needle,
    114           size_t nlen)
    115 {
    116   if ( (0 == nlen) ||
    117        (nlen > hlen) )
    118     return NULL;
    119   for (size_t i = 0; i + nlen <= hlen; i++)
    120     if (0 == memcmp (&hay[i],
    121                      needle,
    122                      nlen))
    123       return &hay[i];
    124   return NULL;
    125 }
    126 
    127 
    128 /**
    129  * Is @a c a character that may appear in an XML name?
    130  *
    131  * @param c character to test
    132  * @return 1 if @a c may be part of a name
    133  */
    134 static int
    135 is_name_char (char c)
    136 {
    137   return ( ( ('a' <= c) && ('z' >= c) ) ||
    138            ( ('A' <= c) && ('Z' >= c) ) ||
    139            ( ('0' <= c) && ('9' >= c) ) ||
    140            ('_' == c) || ('-' == c) || ('.' == c) || (':' == c) );
    141 }
    142 
    143 
    144 /**
    145  * Is @a c XML white space?
    146  *
    147  * @param c character to test
    148  * @return 1 if @a c is white space
    149  */
    150 static int
    151 is_space (char c)
    152 {
    153   return (' ' == c) || ('\t' == c) || ('\r' == c) || ('\n' == c);
    154 }
    155 
    156 
    157 /**
    158  * Append @a cp to @a out as UTF-8.
    159  *
    160  * @param out output buffer
    161  * @param outsz number of bytes available in @a out
    162  * @param pos current write position
    163  * @param cp code point to append
    164  * @return new write position, unchanged if @a cp did not fit
    165  */
    166 static size_t
    167 utf8_put (char *out,
    168           size_t outsz,
    169           size_t pos,
    170           uint32_t cp)
    171 {
    172   if ( (cp > 0x10FFFF) ||
    173        ( (0xD800 <= cp) && (0xDFFF >= cp) ) )
    174     return pos;
    175   if (cp < 0x80)
    176   {
    177     if (pos + 1 > outsz)
    178       return pos;
    179     out[pos++] = (char) cp;
    180   }
    181   else if (cp < 0x800)
    182   {
    183     if (pos + 2 > outsz)
    184       return pos;
    185     out[pos++] = (char) (0xC0 | (cp >> 6));
    186     out[pos++] = (char) (0x80 | (cp & 0x3F));
    187   }
    188   else if (cp < 0x10000)
    189   {
    190     if (pos + 3 > outsz)
    191       return pos;
    192     out[pos++] = (char) (0xE0 | (cp >> 12));
    193     out[pos++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    194     out[pos++] = (char) (0x80 | (cp & 0x3F));
    195   }
    196   else
    197   {
    198     if (pos + 4 > outsz)
    199       return pos;
    200     out[pos++] = (char) (0xF0 | (cp >> 18));
    201     out[pos++] = (char) (0x80 | ((cp >> 12) & 0x3F));
    202     out[pos++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    203     out[pos++] = (char) (0x80 | (cp & 0x3F));
    204   }
    205   return pos;
    206 }
    207 
    208 
    209 /**
    210  * Copy XML character data, resolving the five predefined entities and
    211  * numeric character references and collapsing runs of white space.
    212  *
    213  * The OPF fields we read are laid out across indented lines, so without
    214  * the white space collapsing every value would come back with the
    215  * source file's line breaks embedded in it.
    216  *
    217  * @param data the character data
    218  * @param len number of bytes in @a data
    219  * @param[out] out where to write the result, NUL-terminated
    220  * @param outsz number of bytes available in @a out, at least 2
    221  * @return number of bytes written, not counting the NUL
    222  */
    223 static size_t
    224 xml_text (const char *data,
    225           size_t len,
    226           char *out,
    227           size_t outsz)
    228 {
    229   size_t o = 0;
    230   size_t i = 0;
    231   int pending_space = 0;
    232 
    233   while ( (i < len) &&
    234           (o + 1 < outsz) )
    235   {
    236     if (is_space (data[i]))
    237     {
    238       if (0 != o)
    239         pending_space = 1;
    240       i++;
    241       continue;
    242     }
    243     if (pending_space)
    244     {
    245       out[o++] = ' ';
    246       pending_space = 0;
    247       if (o + 1 >= outsz)
    248         break;
    249     }
    250     if ('&' != data[i])
    251     {
    252       out[o++] = data[i++];
    253       continue;
    254     }
    255     {
    256       const char *semi = memchr (&data[i],
    257                                  ';',
    258                                  len - i);
    259       size_t elen;
    260 
    261       if ( (NULL == semi) ||
    262            ( (size_t) (semi - &data[i]) > 12) )
    263       {
    264         out[o++] = data[i++];   /* a bare '&', which is not legal XML
    265                                    but does occur; pass it through */
    266         continue;
    267       }
    268       elen = (size_t) (semi - &data[i]) - 1;   /* between '&' and ';' */
    269       if ( (3 == elen) && (0 == memcmp (&data[i + 1], "amp", 3)) )
    270         out[o++] = '&';
    271       else if ( (2 == elen) && (0 == memcmp (&data[i + 1], "lt", 2)) )
    272         out[o++] = '<';
    273       else if ( (2 == elen) && (0 == memcmp (&data[i + 1], "gt", 2)) )
    274         out[o++] = '>';
    275       else if ( (4 == elen) && (0 == memcmp (&data[i + 1], "quot", 4)) )
    276         out[o++] = '"';
    277       else if ( (4 == elen) && (0 == memcmp (&data[i + 1], "apos", 4)) )
    278         out[o++] = '\'';
    279       else if ( (2 <= elen) && ('#' == data[i + 1]) )
    280       {
    281         uint32_t cp = 0;
    282         size_t k = i + 2;
    283         int base = 10;
    284 
    285         if ( ('x' == data[k]) || ('X' == data[k]) )
    286         {
    287           base = 16;
    288           k++;
    289         }
    290         for (; k < (size_t) (semi - data); k++)
    291         {
    292           unsigned int d;
    293 
    294           if ( ('0' <= data[k]) && ('9' >= data[k]) )
    295             d = (unsigned int) (data[k] - '0');
    296           else if ( (16 == base) && ('a' <= data[k]) && ('f' >= data[k]) )
    297             d = (unsigned int) (data[k] - 'a') + 10;
    298           else if ( (16 == base) && ('A' <= data[k]) && ('F' >= data[k]) )
    299             d = (unsigned int) (data[k] - 'A') + 10;
    300           else
    301           {
    302             cp = 0;
    303             break;
    304           }
    305           if (cp > 0x10FFFF)
    306           {
    307             cp = 0;
    308             break;
    309           }
    310           cp = cp * (uint32_t) base + d;
    311         }
    312         o = utf8_put (out,
    313                       outsz - 1,
    314                       o,
    315                       cp);
    316       }
    317       else
    318       {
    319         /* an entity we do not know; drop it rather than guess */
    320       }
    321       i += elen + 2;
    322     }
    323   }
    324   out[o] = '\0';
    325   return o;
    326 }
    327 
    328 
    329 /**
    330  * Read the value of attribute @a name out of the body of a start tag.
    331  * A namespace prefix on the attribute is ignored, so a request for
    332  * `role' also matches `opf:role'.
    333  *
    334  * @param data the start tag body, after the element name
    335  * @param len number of bytes in @a data
    336  * @param name local name of the attribute
    337  * @param[out] out where to write the value, NUL-terminated
    338  * @param outsz number of bytes available in @a out, at least 2
    339  * @return 1 if the attribute was found, 0 if not
    340  */
    341 static int
    342 xml_attr (const char *data,
    343           size_t len,
    344           const char *name,
    345           char *out,
    346           size_t outsz)
    347 {
    348   size_t nlen = strlen (name);
    349 
    350   out[0] = '\0';
    351   if (nlen >= len)
    352     return 0;
    353   for (size_t i = 0; i + nlen < len; i++)
    354   {
    355     size_t k;
    356     char quote;
    357     const char *vend;
    358 
    359     if (0 != memcmp (&data[i],
    360                      name,
    361                      nlen))
    362       continue;
    363     /* the name must start where an attribute may start: right after
    364        white space, or right after a namespace prefix that does */
    365     if (0 == i)
    366       continue;
    367     if (is_space (data[i - 1]))
    368     {
    369       /* fine */
    370     }
    371     else if (':' == data[i - 1])
    372     {
    373       size_t j = i - 1;
    374 
    375       while ( (0 < j) &&
    376               is_name_char (data[j - 1]) &&
    377               (':' != data[j - 1]) )
    378         j--;
    379       if ( (0 == j) ||
    380            (! is_space (data[j - 1])) )
    381         continue;
    382     }
    383     else
    384     {
    385       continue;
    386     }
    387     k = i + nlen;
    388     while ( (k < len) &&
    389             is_space (data[k]) )
    390       k++;
    391     if ( (k >= len) ||
    392          ('=' != data[k]) )
    393       continue;
    394     k++;
    395     while ( (k < len) &&
    396             is_space (data[k]) )
    397       k++;
    398     if ( (k >= len) ||
    399          ( ('"' != data[k]) && ('\'' != data[k]) ) )
    400       continue;
    401     quote = data[k];
    402     k++;
    403     vend = memchr (&data[k],
    404                    quote,
    405                    len - k);
    406     if (NULL == vend)
    407       return 0;
    408     (void) xml_text (&data[k],
    409                      (size_t) (vend - &data[k]),
    410                      out,
    411                      outsz);
    412     return 1;
    413   }
    414   return 0;
    415 }
    416 
    417 
    418 /**
    419  * Does @a s, after an optional `urn:isbn:' prefix, look like an ISBN?
    420  *
    421  * @param s NUL-terminated candidate
    422  * @return 1 if @a s is 10 or 13 digits with optional separators
    423  */
    424 static int
    425 looks_like_isbn (const char *s)
    426 {
    427   size_t digits = 0;
    428 
    429   if (0 == strncasecmp (s,
    430                         "urn:isbn:",
    431                         strlen ("urn:isbn:")))
    432     s += strlen ("urn:isbn:");
    433   else if (0 == strncasecmp (s,
    434                              "isbn:",
    435                              strlen ("isbn:")))
    436     s += strlen ("isbn:");
    437   for (size_t i = 0; '\0' != s[i]; i++)
    438   {
    439     if ( ('-' == s[i]) || (' ' == s[i]) )
    440       continue;
    441     if ( ('0' <= s[i]) && ('9' >= s[i]) )
    442     {
    443       digits++;
    444       continue;
    445     }
    446     if ( ( ('X' == s[i]) || ('x' == s[i]) ) &&
    447          (9 == digits) &&
    448          ('\0' == s[i + 1]) )
    449     {
    450       digits++;
    451       continue;
    452     }
    453     return 0;
    454   }
    455   return (10 == digits) || (13 == digits);
    456 }
    457 
    458 
    459 /**
    460  * Read the current member of a ZIP archive.
    461  *
    462  * @param uf the archive, positioned on the member
    463  * @param cap never read more than this many bytes
    464  * @param[out] len number of bytes read
    465  * @return the member's contents with a NUL appended, NULL on error;
    466  *         caller must free
    467  */
    468 static char *
    469 read_current (struct EXTRACTOR_UnzipFile *uf,
    470               size_t cap,
    471               size_t *len)
    472 {
    473   struct EXTRACTOR_UnzipFileInfo fi;
    474   char *buf;
    475   size_t size;
    476   size_t got = 0;
    477 
    478   *len = 0;
    479   if (EXTRACTOR_UNZIP_OK !=
    480       EXTRACTOR_common_unzip_get_current_file_info (uf,
    481                                                     &fi,
    482                                                     NULL, 0,
    483                                                     NULL, 0,
    484                                                     NULL, 0))
    485     return NULL;
    486   size = (size_t) fi.uncompressed_size;
    487   if (0 == size)
    488     return NULL;
    489   if (size > cap)
    490     size = cap;
    491   if (NULL == (buf = malloc (size + 1)))
    492     return NULL;
    493   if (EXTRACTOR_UNZIP_OK !=
    494       EXTRACTOR_common_unzip_open_current_file (uf))
    495   {
    496     free (buf);
    497     return NULL;
    498   }
    499   while (got < size)
    500   {
    501     ssize_t ret;
    502 
    503     ret = EXTRACTOR_common_unzip_read_current_file (uf,
    504                                                     &buf[got],
    505                                                     size - got);
    506     if (0 >= ret)
    507       break;   /* error or end of member */
    508     if (((size_t) ret) > size - got)
    509       break;   /* cannot happen, but do not overrun if it does */
    510     got += (size_t) ret;
    511   }
    512   (void) EXTRACTOR_common_unzip_close_current_file (uf);
    513   if (0 == got)
    514   {
    515     free (buf);
    516     return NULL;
    517   }
    518   buf[got] = '\0';
    519   *len = got;
    520   return buf;
    521 }
    522 
    523 
    524 /**
    525  * Read a whole member out of a ZIP archive.
    526  *
    527  * @param uf the archive
    528  * @param name member to read
    529  * @param cap never read more than this many bytes
    530  * @param[out] len number of bytes read
    531  * @return the member's contents with a NUL appended, NULL on error;
    532  *         caller must free
    533  */
    534 static char *
    535 read_member (struct EXTRACTOR_UnzipFile *uf,
    536              const char *name,
    537              size_t cap,
    538              size_t *len)
    539 {
    540   *len = 0;
    541   if (EXTRACTOR_UNZIP_OK !=
    542       EXTRACTOR_common_unzip_go_find_local_file (uf,
    543                                                  name,
    544                                                  2))
    545     return NULL;
    546   return read_current (uf,
    547                        cap,
    548                        len);
    549 }
    550 
    551 
    552 /**
    553  * Does the archive contain a member with this name?
    554  *
    555  * @param uf the archive
    556  * @param name member to look for
    557  * @return 1 if present
    558  */
    559 static int
    560 has_member (struct EXTRACTOR_UnzipFile *uf,
    561             const char *name)
    562 {
    563   return (EXTRACTOR_UNZIP_OK ==
    564           EXTRACTOR_common_unzip_go_find_local_file (uf,
    565                                                      name,
    566                                                      2));
    567 }
    568 
    569 
    570 /**
    571  * Walk the `<metadata>' element of an OPF package document and report
    572  * what it holds.
    573  *
    574  * This is a bounded text scan, not an XML parser: the OPF is untrusted
    575  * input and everything we want out of it is a flat list of leaf
    576  * elements, so pulling in a parser would buy nothing but attack surface.
    577  *
    578  * @param ec extraction context
    579  * @param opf the package document
    580  * @param len number of bytes in @a opf
    581  * @return 1 if the caller should stop extracting, 0 to continue
    582  */
    583 static int
    584 parse_opf_metadata (struct EXTRACTOR_ExtractContext *ec,
    585                     const char *opf,
    586                     size_t len)
    587 {
    588   char value[EXTRACTOR_FORENSIC_MAX_STRING];
    589   char attr[256];
    590   const char *mstart;
    591   const char *mend;
    592   const char *p;
    593   unsigned int elements = 0;
    594   /* the repeating elements are capped separately: a file with ten
    595      thousand <dc:subject> entries is characterised well enough by the
    596      first few dozen */
    597   unsigned int subjects = 0;
    598   unsigned int identifiers = 0;
    599   unsigned int people = 0;
    600 
    601   mstart = mem_find (opf,
    602                      len,
    603                      "<metadata",
    604                      strlen ("<metadata"));
    605   if (NULL == mstart)
    606     return 0;
    607   mend = mem_find (mstart,
    608                    len - (size_t) (mstart - opf),
    609                    "</metadata",
    610                    strlen ("</metadata"));
    611   if (NULL == mend)
    612     mend = opf + len;
    613   p = mstart;
    614   while ( (p < mend) &&
    615           (elements++ < MAX_ELEMENTS) )
    616   {
    617     const char *name;
    618     const char *local;
    619     const char *body;
    620     const char *text;
    621     size_t name_len;
    622     size_t body_len;
    623     size_t text_len;
    624     int self_closing;
    625     char quote = '\0';
    626     enum EXTRACTOR_MetaType type;
    627 
    628     p = memchr (p,
    629                 '<',
    630                 (size_t) (mend - p));
    631     if (NULL == p)
    632       break;
    633     p++;
    634     if (p >= mend)
    635       break;
    636     if ( ('/' == *p) || ('?' == *p) || ('!' == *p) )
    637       continue;
    638     name = p;
    639     while ( (p < mend) &&
    640             is_name_char (*p) )
    641       p++;
    642     name_len = (size_t) (p - name);
    643     if (0 == name_len)
    644       continue;
    645     body = p;
    646     while (p < mend)
    647     {
    648       if ('\0' != quote)
    649       {
    650         if (*p == quote)
    651           quote = '\0';
    652       }
    653       else if ( ('"' == *p) || ('\'' == *p) )
    654       {
    655         quote = *p;
    656       }
    657       else if ('>' == *p)
    658       {
    659         break;
    660       }
    661       p++;
    662     }
    663     if (p >= mend)
    664       break;
    665     body_len = (size_t) (p - body);
    666     self_closing = ( (0 < body_len) && ('/' == p[-1]) );
    667     if (self_closing)
    668       body_len--;
    669     p++;   /* step past '>' */
    670     text = p;
    671     text_len = 0;
    672     if (! self_closing)
    673     {
    674       const char *e = memchr (p,
    675                               '<',
    676                               (size_t) (mend - p));
    677 
    678       text_len = (NULL == e)
    679                  ? (size_t) (mend - p)
    680                  : (size_t) (e - p);
    681     }
    682     /* strip the namespace prefix; `dc:title' and `title' are the same
    683        element for our purposes because we are already inside
    684        <metadata> */
    685     local = name;
    686     for (size_t i = 0; i < name_len; i++)
    687       if (':' == name[i])
    688         local = &name[i + 1];
    689     name_len -= (size_t) (local - name);
    690 
    691 #define IS(s) ( (strlen (s) == name_len) && \
    692                 (0 == memcmp (local, s, name_len)) )
    693 
    694     if (IS ("meta"))
    695     {
    696       if ( (xml_attr (body,
    697                       body_len,
    698                       "name",
    699                       attr,
    700                       sizeof (attr))) &&
    701            (xml_attr (body,
    702                       body_len,
    703                       "content",
    704                       value,
    705                       sizeof (value))) )
    706       {
    707         if (0 == strcasecmp (attr,
    708                              "calibre:timestamp"))
    709           type = EXTRACTOR_METATYPE_CREATION_DATE;
    710         else if ( (0 == strcasecmp (attr,
    711                                     "generator")) ||
    712                   (0 == strcasecmp (attr,
    713                                     "calibre:generator")) )
    714           type = EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE;
    715         else
    716           continue;
    717         if (0 !=
    718             EXTRACTOR_forensic_emit_text_ (ec,
    719                                            PLUGIN_NAME,
    720                                            type,
    721                                            value,
    722                                            strlen (value)))
    723           return 1;
    724       }
    725       else if (xml_attr (body,
    726                          body_len,
    727                          "property",
    728                          attr,
    729                          sizeof (attr)))
    730       {
    731         if (0 != strcasecmp (attr,
    732                              "dcterms:modified"))
    733           continue;
    734         (void) xml_text (text,
    735                          text_len,
    736                          value,
    737                          sizeof (value));
    738         if (0 !=
    739             EXTRACTOR_forensic_emit_text_ (
    740               ec,
    741               PLUGIN_NAME,
    742               EXTRACTOR_METATYPE_MODIFICATION_DATE,
    743               value,
    744               strlen (value)))
    745           return 1;
    746       }
    747       continue;
    748     }
    749     if (0 == text_len)
    750       continue;
    751     (void) xml_text (text,
    752                      text_len,
    753                      value,
    754                      sizeof (value));
    755     if ('\0' == value[0])
    756       continue;
    757     if (IS ("title"))
    758       type = EXTRACTOR_METATYPE_TITLE;
    759     else if (IS ("creator"))
    760     {
    761       if (people++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    762         continue;
    763       type = EXTRACTOR_METATYPE_AUTHOR_NAME;
    764     }
    765     else if (IS ("publisher"))
    766       type = EXTRACTOR_METATYPE_PUBLISHER;
    767     else if (IS ("language"))
    768       type = EXTRACTOR_METATYPE_LANGUAGE;
    769     else if (IS ("date"))
    770       type = EXTRACTOR_METATYPE_PUBLICATION_DATE;
    771     else if (IS ("subject"))
    772     {
    773       if (subjects++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    774         continue;
    775       type = EXTRACTOR_METATYPE_KEYWORDS;
    776     }
    777     else if (IS ("description"))
    778       type = EXTRACTOR_METATYPE_DESCRIPTION;
    779     else if (IS ("rights"))
    780       type = EXTRACTOR_METATYPE_RIGHTS;
    781     else if (IS ("source"))
    782       type = EXTRACTOR_METATYPE_URI;
    783     else if (IS ("contributor"))
    784     {
    785       /* `bkp' is the MARC relator for the agent that made the file --
    786          which is the tool, and thus the provenance we are after */
    787       if ( (xml_attr (body,
    788                       body_len,
    789                       "role",
    790                       attr,
    791                       sizeof (attr))) &&
    792            (0 == strcasecmp (attr,
    793                              "bkp")) )
    794       {
    795         type = EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE;
    796       }
    797       else
    798       {
    799         if (people++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    800           continue;
    801         type = EXTRACTOR_METATYPE_CONTRIBUTOR_NAME;
    802       }
    803     }
    804     else if (IS ("identifier"))
    805     {
    806       int isbn = 0;
    807 
    808       if (identifiers++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    809         continue;
    810 
    811       if (xml_attr (body,
    812                     body_len,
    813                     "scheme",
    814                     attr,
    815                     sizeof (attr)))
    816         isbn = (0 == strcasecmp (attr,
    817                                  "ISBN"));
    818       if (! isbn)
    819         isbn = looks_like_isbn (value);
    820       type = isbn
    821              ? EXTRACTOR_METATYPE_ISBN
    822              : EXTRACTOR_METATYPE_URI;
    823     }
    824     else
    825     {
    826       continue;
    827     }
    828     if (0 !=
    829         EXTRACTOR_forensic_emit_text_ (ec,
    830                                        PLUGIN_NAME,
    831                                        type,
    832                                        value,
    833                                        strlen (value)))
    834       return 1;
    835   }
    836 #undef IS
    837   return 0;
    838 }
    839 
    840 
    841 /* ------------------------------------------------------------------ */
    842 /* Full text: character sets                                           */
    843 /* ------------------------------------------------------------------ */
    844 
    845 /**
    846  * Never inflate more than this from one EPUB content document.
    847  */
    848 #define MAX_CHAPTER (8 * 1024 * 1024)
    849 
    850 /**
    851  * Never inflate more than this from all EPUB content documents
    852  * together, nor decompress more than this from the text records of a
    853  * MOBI file.  The full text itself is capped at #EXTRACTOR_FULLTEXT_MAX,
    854  * and markup rarely more than triples the size of the text.
    855  */
    856 #define MAX_BOOK_MARKUP (64 * 1024 * 1024)
    857 
    858 /**
    859  * Upper bound on the number of manifest items we keep.
    860  */
    861 #define MAX_MANIFEST 4096
    862 
    863 /**
    864  * Upper bound on the number of spine entries we follow.
    865  */
    866 #define MAX_SPINE 4096
    867 
    868 /**
    869  * Upper bound on the size of one MOBI text record (normally 4096 bytes
    870  * of text plus a few trailing bytes).
    871  */
    872 #define MAX_TEXT_RECORD (64 * 1024)
    873 
    874 /**
    875  * Upper bound on what one MOBI text record may decompress to.
    876  */
    877 #define MAX_TEXT_RECORD_OUT (64 * 1024)
    878 
    879 /**
    880  * How many attributes of one element we look at.
    881  */
    882 #define MAX_ATTRS 64
    883 
    884 
    885 /**
    886  * Code points of windows-1252 bytes 0x80 to 0x9F.
    887  */
    888 static const uint16_t cp1252[32] = {
    889   0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
    890   0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F,
    891   0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
    892   0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178
    893 };
    894 
    895 
    896 /**
    897  * Convert windows-1252 (a superset of ISO-8859-1 in the printable
    898  * range) to UTF-8.
    899  *
    900  * @param data input
    901  * @param len number of bytes in @a data
    902  * @param[out] olen number of bytes in the result
    903  * @return converted text, NULL on allocation failure; caller frees
    904  */
    905 static char *
    906 cp1252_to_utf8 (const char *data,
    907                 size_t len,
    908                 size_t *olen)
    909 {
    910   char *out;
    911   size_t o = 0;
    912 
    913   if (NULL == (out = malloc (3 * len + 1)))
    914     return NULL;
    915   for (size_t i = 0; i < len; i++)
    916   {
    917     unsigned char c = (unsigned char) data[i];
    918 
    919     if (c < 0x80)
    920       out[o++] = (char) c;
    921     else if (c < 0xA0)
    922       o = utf8_put (out, 3 * len, o, cp1252[c - 0x80]);
    923     else
    924       o = utf8_put (out, 3 * len, o, c);
    925   }
    926   *olen = o;
    927   return out;
    928 }
    929 
    930 
    931 /**
    932  * Convert UTF-16 to UTF-8.  Unpaired surrogates become U+FFFD.
    933  *
    934  * @param data input
    935  * @param len number of bytes in @a data
    936  * @param big_endian 1 for UTF-16BE, 0 for UTF-16LE
    937  * @param[out] olen number of bytes in the result
    938  * @return converted text, NULL on allocation failure; caller frees
    939  */
    940 static char *
    941 utf16_to_utf8 (const char *data,
    942                size_t len,
    943                int big_endian,
    944                size_t *olen)
    945 {
    946   const unsigned char *u = (const unsigned char *) data;
    947   char *out;
    948   size_t o = 0;
    949   size_t cap = 2 * len + 4;   /* 2 bytes in never make more than 3 out */
    950 
    951   if (NULL == (out = malloc (cap)))
    952     return NULL;
    953   for (size_t i = 0; i + 1 < len; i += 2)
    954   {
    955     uint32_t cp = big_endian
    956                   ? (uint32_t) ((u[i] << 8) | u[i + 1])
    957                   : (uint32_t) ((u[i + 1] << 8) | u[i]);
    958 
    959     if ( (cp >= 0xD800) && (cp <= 0xDBFF) && (i + 3 < len) )
    960     {
    961       uint32_t lo = big_endian
    962                     ? (uint32_t) ((u[i + 2] << 8) | u[i + 3])
    963                     : (uint32_t) ((u[i + 3] << 8) | u[i + 2]);
    964 
    965       if ( (lo >= 0xDC00) && (lo <= 0xDFFF) )
    966       {
    967         cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
    968         i += 2;
    969       }
    970     }
    971     if ( (cp >= 0xD800) && (cp <= 0xDFFF) )
    972       cp = 0xFFFD;
    973     o = utf8_put (out, cap, o, cp);
    974   }
    975   *olen = o;
    976   return out;
    977 }
    978 
    979 
    980 /**
    981  * Convert @a data from the named character set to UTF-8.  UTF-8 input
    982  * is copied as is (invalid sequences are dealt with when the text is
    983  * appended to the full text); UTF-16 and windows-1252 are converted
    984  * here, anything else by iconv if we have it.
    985  *
    986  * @param data input
    987  * @param len number of bytes in @a data
    988  * @param cs lower-case charset name
    989  * @param[out] olen number of bytes in the result
    990  * @return converted text, NULL on allocation failure; caller frees
    991  */
    992 static char *
    993 convert_to_utf8 (const char *data,
    994                  size_t len,
    995                  const char *cs,
    996                  size_t *olen)
    997 {
    998   char *out;
    999 
   1000   *olen = 0;
   1001   if ( (0 == strcmp (cs, "iso-8859-1")) ||
   1002        (0 == strcmp (cs, "iso8859-1")) ||
   1003        (0 == strcmp (cs, "latin1")) ||
   1004        (0 == strcmp (cs, "us-ascii")) ||
   1005        (0 == strcmp (cs, "ascii")) ||
   1006        (0 == strcmp (cs, "cp1252")) ||
   1007        (0 == strcmp (cs, "windows-1252")) )
   1008     return cp1252_to_utf8 (data, len, olen);
   1009   if ( (0 == strcmp (cs, "utf-16be")) ||
   1010        (0 == strcmp (cs, "utf-16")) )
   1011     return utf16_to_utf8 (data, len, 1, olen);
   1012   if (0 == strcmp (cs, "utf-16le"))
   1013     return utf16_to_utf8 (data, len, 0, olen);
   1014 #if HAVE_ICONV
   1015   if ( ('\0' != cs[0]) &&
   1016        (0 != strcmp (cs, "utf-8")) &&
   1017        (0 != strcmp (cs, "utf8")) )
   1018   {
   1019     iconv_t cd = iconv_open ("UTF-8", cs);
   1020 
   1021     if ((iconv_t) -1 != cd)
   1022     {
   1023       char *ib = (char *) data;
   1024       size_t il = len;
   1025       char *ob;
   1026       size_t ol;
   1027 
   1028       /* no charset produces more than 4 bytes of UTF-8 per byte */
   1029       if (NULL == (out = malloc (4 * len + 4)))
   1030       {
   1031         iconv_close (cd);
   1032         return NULL;
   1033       }
   1034       ob = out;
   1035       ol = 4 * len;
   1036       while (il > 0)
   1037       {
   1038         if ((size_t) -1 !=
   1039             iconv (cd, &ib, &il, &ob, &ol))
   1040           break;
   1041         if ( (E2BIG == errno) ||
   1042              (ol < 3) )
   1043           break;
   1044         /* skip one undecodable byte */
   1045         *ob++ = (char) 0xEF;
   1046         *ob++ = (char) 0xBF;
   1047         *ob++ = (char) 0xBD;
   1048         ol -= 3;
   1049         ib++;
   1050         il--;
   1051       }
   1052       iconv_close (cd);
   1053       *olen = (size_t) (ob - out);
   1054       return out;
   1055     }
   1056   }
   1057 #endif
   1058   if (NULL == (out = malloc (len + 1)))
   1059     return NULL;
   1060   memcpy (out, data, len);
   1061   *olen = len;
   1062   return out;
   1063 }
   1064 
   1065 
   1066 /**
   1067  * Read a charset name after `encoding' or `charset' at @a p: optional
   1068  * white space, '=', optional white space and quotes, then the name.
   1069  *
   1070  * @param p where to start
   1071  * @param end end of the buffer
   1072  * @param[out] cs where to write the lower-case name
   1073  * @param cs_size number of bytes available in @a cs
   1074  * @return 1 if a name was found
   1075  */
   1076 static int
   1077 charset_value (const char *p,
   1078                const char *end,
   1079                char *cs,
   1080                size_t cs_size)
   1081 {
   1082   size_t o = 0;
   1083 
   1084   while ( (p < end) && is_space (*p))
   1085     p++;
   1086   if ( (p >= end) || ('=' != *p) )
   1087     return 0;
   1088   p++;
   1089   while ( (p < end) &&
   1090           (is_space (*p) || ('"' == *p) || ('\'' == *p) ) )
   1091     p++;
   1092   while ( (p < end) &&
   1093           (o + 1 < cs_size) &&
   1094           (isalnum ((unsigned char) *p) ||
   1095            ('-' == *p) || ('_' == *p) || ('.' == *p) || (':' == *p) ) )
   1096     cs[o++] = (char) tolower ((unsigned char) *p++);
   1097   cs[o] = '\0';
   1098   return (0 != o);
   1099 }
   1100 
   1101 
   1102 /**
   1103  * Convert an EPUB content document to UTF-8: honour a byte order mark,
   1104  * UTF-16 without one, and the encoding in the XML declaration or a
   1105  * META charset declaration; default to UTF-8.
   1106  *
   1107  * @param data the document
   1108  * @param len number of bytes in @a data
   1109  * @param[out] olen number of bytes in the result
   1110  * @return the document in UTF-8, NULL on allocation failure
   1111  */
   1112 static char *
   1113 xhtml_to_utf8 (const char *data,
   1114                size_t len,
   1115                size_t *olen)
   1116 {
   1117   const unsigned char *u = (const unsigned char *) data;
   1118   size_t scan = (len > 1024) ? 1024 : len;
   1119   char cs[64];
   1120 
   1121   if ( (len >= 3) && (0xEF == u[0]) && (0xBB == u[1]) && (0xBF == u[2]) )
   1122     return convert_to_utf8 (&data[3], len - 3, "utf-8", olen);
   1123   if ( (len >= 2) && (0xFE == u[0]) && (0xFF == u[1]) )
   1124     return convert_to_utf8 (&data[2], len - 2, "utf-16be", olen);
   1125   if ( (len >= 2) && (0xFF == u[0]) && (0xFE == u[1]) )
   1126     return convert_to_utf8 (&data[2], len - 2, "utf-16le", olen);
   1127   if ( (len >= 4) && (0 == u[0]) && ('<' == u[1]) && (0 == u[2]) )
   1128     return convert_to_utf8 (data, len, "utf-16be", olen);
   1129   if ( (len >= 4) && ('<' == u[0]) && (0 == u[1]) && (0 == u[3]) )
   1130     return convert_to_utf8 (data, len, "utf-16le", olen);
   1131   cs[0] = '\0';
   1132   if ( (scan > 5) && (0 == memcmp (data, "<?xml", 5)) )
   1133   {
   1134     const char *gt = memchr (data, '>', scan);
   1135     size_t dlen = (NULL == gt) ? scan : (size_t) (gt - data);
   1136     const char *e = mem_find (data, dlen, "encoding", strlen ("encoding"));
   1137 
   1138     if (NULL != e)
   1139       (void) charset_value (e + strlen ("encoding"),
   1140                             data + dlen,
   1141                             cs,
   1142                             sizeof (cs));
   1143   }
   1144   if ('\0' == cs[0])
   1145   {
   1146     const char *m = mem_find (data, scan, "charset", strlen ("charset"));
   1147 
   1148     if (NULL != m)
   1149       (void) charset_value (m + strlen ("charset"),
   1150                             data + scan,
   1151                             cs,
   1152                             sizeof (cs));
   1153   }
   1154   /* a document in UTF-8 that claims UTF-16 cannot be UTF-16: we would
   1155      have seen the zero bytes */
   1156   if (0 == strncmp (cs, "utf-16", 6))
   1157     cs[0] = '\0';
   1158   return convert_to_utf8 (data, len, cs, olen);
   1159 }
   1160 
   1161 
   1162 /* ------------------------------------------------------------------ */
   1163 /* Full text: (X)HTML markup                                           */
   1164 /* ------------------------------------------------------------------ */
   1165 
   1166 /**
   1167  * HTML named character references for U+00A0 to U+00FF, in order.
   1168  */
   1169 static const char *const latin1_entities[96] = {
   1170   "nbsp", "iexcl", "cent", "pound", "curren", "yen", "brvbar", "sect",
   1171   "uml", "copy", "ordf", "laquo", "not", "shy", "reg", "macr",
   1172   "deg", "plusmn", "sup2", "sup3", "acute", "micro", "para", "middot",
   1173   "cedil", "sup1", "ordm", "raquo", "frac14", "frac12", "frac34", "iquest",
   1174   "Agrave", "Aacute", "Acirc", "Atilde", "Auml", "Aring", "AElig", "Ccedil",
   1175   "Egrave", "Eacute", "Ecirc", "Euml", "Igrave", "Iacute", "Icirc", "Iuml",
   1176   "ETH", "Ntilde", "Ograve", "Oacute", "Ocirc", "Otilde", "Ouml", "times",
   1177   "Oslash", "Ugrave", "Uacute", "Ucirc", "Uuml", "Yacute", "THORN", "szlig",
   1178   "agrave", "aacute", "acirc", "atilde", "auml", "aring", "aelig", "ccedil",
   1179   "egrave", "eacute", "ecirc", "euml", "igrave", "iacute", "icirc", "iuml",
   1180   "eth", "ntilde", "ograve", "oacute", "ocirc", "otilde", "ouml", "divide",
   1181   "oslash", "ugrave", "uacute", "ucirc", "uuml", "yacute", "thorn", "yuml"
   1182 };
   1183 
   1184 
   1185 /**
   1186  * Other HTML named character references common in books.
   1187  */
   1188 static const struct
   1189 {
   1190   /**
   1191    * Entity name.
   1192    */
   1193   const char *name;
   1194 
   1195   /**
   1196    * Code point.
   1197    */
   1198   uint32_t cp;
   1199 } other_entities[] = {
   1200   { "amp", '&' }, { "lt", '<' }, { "gt", '>' }, { "quot", '"' },
   1201   { "apos", '\'' }, { "OElig", 0x152 }, { "oelig", 0x153 },
   1202   { "Scaron", 0x160 }, { "scaron", 0x161 }, { "Yuml", 0x178 },
   1203   { "fnof", 0x192 }, { "circ", 0x2C6 }, { "tilde", 0x2DC },
   1204   { "ensp", 0x2002 }, { "emsp", 0x2003 }, { "thinsp", 0x2009 },
   1205   { "zwnj", 0x200C }, { "zwj", 0x200D }, { "lrm", 0x200E },
   1206   { "rlm", 0x200F }, { "ndash", 0x2013 }, { "mdash", 0x2014 },
   1207   { "lsquo", 0x2018 }, { "rsquo", 0x2019 }, { "sbquo", 0x201A },
   1208   { "ldquo", 0x201C }, { "rdquo", 0x201D }, { "bdquo", 0x201E },
   1209   { "dagger", 0x2020 }, { "Dagger", 0x2021 }, { "bull", 0x2022 },
   1210   { "hellip", 0x2026 }, { "permil", 0x2030 }, { "prime", 0x2032 },
   1211   { "Prime", 0x2033 }, { "lsaquo", 0x2039 }, { "rsaquo", 0x203A },
   1212   { "oline", 0x203E }, { "euro", 0x20AC }, { "trade", 0x2122 },
   1213   { "larr", 0x2190 }, { "rarr", 0x2192 }, { "minus", 0x2212 },
   1214   { NULL, 0 }
   1215 };
   1216 
   1217 
   1218 /**
   1219  * Decode the character reference at @a p (just after the '&').
   1220  *
   1221  * @param p text after the '&'
   1222  * @param len number of bytes available at @a p
   1223  * @param[out] cp the code point
   1224  * @return number of bytes consumed after the '&' (including the ';'),
   1225  *         0 if this is not a character reference we know
   1226  */
   1227 static size_t
   1228 decode_entity (const char *p,
   1229                size_t len,
   1230                uint32_t *cp)
   1231 {
   1232   size_t n = 0;
   1233 
   1234   if ( (len >= 2) && ('#' == p[0]) )
   1235   {
   1236     uint32_t v = 0;
   1237     int base = 10;
   1238     size_t k = 1;
   1239     size_t digits = 0;
   1240 
   1241     if ( ('x' == p[1]) || ('X' == p[1]) )
   1242     {
   1243       base = 16;
   1244       k = 2;
   1245     }
   1246     for (; (k < len) && (digits < 8); k++, digits++)
   1247     {
   1248       unsigned int d;
   1249 
   1250       if ( ('0' <= p[k]) && ('9' >= p[k]) )
   1251         d = (unsigned int) (p[k] - '0');
   1252       else if ( (16 == base) && ('a' <= p[k]) && ('f' >= p[k]) )
   1253         d = (unsigned int) (p[k] - 'a') + 10;
   1254       else if ( (16 == base) && ('A' <= p[k]) && ('F' >= p[k]) )
   1255         d = (unsigned int) (p[k] - 'A') + 10;
   1256       else
   1257         break;
   1258       v = v * (uint32_t) base + d;
   1259     }
   1260     if ( (0 == digits) ||
   1261          (k >= len) ||
   1262          (';' != p[k]) )
   1263       return 0;
   1264     *cp = (v > 0x10FFFF) ? 0xFFFD : v;
   1265     return k + 1;
   1266   }
   1267   while ( (n < len) &&
   1268           (n < 10) &&
   1269           isalnum ((unsigned char) p[n]) )
   1270     n++;
   1271   if ( (0 == n) ||
   1272        (n >= len) ||
   1273        (';' != p[n]) )
   1274     return 0;
   1275   for (unsigned int i = 0; i < 96; i++)
   1276     if ( (strlen (latin1_entities[i]) == n) &&
   1277          (0 == memcmp (latin1_entities[i], p, n)) )
   1278     {
   1279       *cp = 0xA0 + i;
   1280       return n + 1;
   1281     }
   1282   for (unsigned int i = 0; NULL != other_entities[i].name; i++)
   1283     if ( (strlen (other_entities[i].name) == n) &&
   1284          (0 == memcmp (other_entities[i].name, p, n)) )
   1285     {
   1286       *cp = other_entities[i].cp;
   1287       return n + 1;
   1288     }
   1289   return 0;
   1290 }
   1291 
   1292 
   1293 /**
   1294  * State of the conversion of one (X)HTML document to text.
   1295  */
   1296 struct MarkupState
   1297 {
   1298   /**
   1299    * Where the text goes.
   1300    */
   1301   struct EXTRACTOR_FullText *ft;
   1302 
   1303   /**
   1304    * Local name of the element whose content we are skipping, "" if
   1305    * none.
   1306    */
   1307   char skip_name[32];
   1308 
   1309   /**
   1310    * Nesting level of @e skip_name elements inside the skipped one.
   1311    */
   1312   unsigned int skip_nest;
   1313 
   1314   /**
   1315    * Number of open sectioning elements (article, section, main, ...);
   1316    * a header or footer inside one is not page furniture.
   1317    */
   1318   unsigned int sections;
   1319 
   1320   /**
   1321    * 1 if the last thing we saw was a <br>, so another one ends the
   1322    * paragraph.
   1323    */
   1324   int after_br;
   1325 };
   1326 
   1327 
   1328 /**
   1329  * Is @a name (lower case) in the 0-terminated list @a list?
   1330  *
   1331  * @param name element name
   1332  * @param list list of names
   1333  * @return 1 if found
   1334  */
   1335 static int
   1336 name_in (const char *name,
   1337          const char *const *list)
   1338 {
   1339   for (unsigned int i = 0; NULL != list[i]; i++)
   1340     if (0 == strcmp (name, list[i]))
   1341       return 1;
   1342   return 0;
   1343 }
   1344 
   1345 
   1346 /**
   1347  * Elements that end a paragraph.
   1348  */
   1349 static const char *const block_elements[] = {
   1350   "address", "article", "aside", "blockquote", "body", "caption",
   1351   "center", "dd", "details", "dialog", "dir", "div", "dl", "dt",
   1352   "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2",
   1353   "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "html", "legend",
   1354   "li", "listing", "main", "menu", "nav", "ol", "p", "pre", "section",
   1355   "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul",
   1356   "xmp", "title", "head", "noscript",
   1357   /* DTBook and FB2-style structure some EPUBs carry */
   1358   "level", "level1", "level2", "level3", "level4", "level5", "level6",
   1359   "hd", "h", "note", "annotation", "epigraph", "poem", "stanza", "v",
   1360   NULL
   1361 };
   1362 
   1363 /**
   1364  * Elements whose content is never document text.
   1365  */
   1366 static const char *const skip_elements[] = {
   1367   "head", "title", "script", "style", "noscript", "template", "nav",
   1368   "aside", "select", "datalist", "iframe", "object", "applet", "rp", "rt",
   1369   "guide", "svg", "math", NULL
   1370 };
   1371 
   1372 /**
   1373  * Elements whose content is not markup (the end tag is searched for
   1374  * literally).
   1375  */
   1376 static const char *const raw_text_elements[] = {
   1377   "script", "style", "xmp", NULL
   1378 };
   1379 
   1380 /**
   1381  * HTML elements without an end tag; they must never open a skipped
   1382  * region.
   1383  */
   1384 static const char *const void_elements[] = {
   1385   "area", "base", "br", "col", "embed", "hr", "img", "input", "link",
   1386   "meta", "param", "source", "track", "wbr", NULL
   1387 };
   1388 
   1389 /**
   1390  * Elements that start a sectioning context.
   1391  */
   1392 static const char *const sectioning_elements[] = {
   1393   "article", "section", "main", "aside", "nav", NULL
   1394 };
   1395 
   1396 
   1397 /**
   1398  * Does a start tag's attribute list mark the element as not part of
   1399  * the text: hidden, aria-hidden="true", a hiding inline style, an ARIA
   1400  * landmark role for the page margins, or a page break marker
   1401  * (epub:type="pagebreak", role="doc-pagebreak")?
   1402  *
   1403  * @param a attribute text of the start tag (after the name)
   1404  * @param len number of bytes in @a a
   1405  * @return 1 to skip the element
   1406  */
   1407 static int
   1408 attrs_skip (const char *a,
   1409             size_t len)
   1410 {
   1411   static const char *const roles[] = {
   1412     "navigation", "banner", "contentinfo", "complementary", "search",
   1413     "doc-pagebreak", NULL
   1414   };
   1415   size_t i = 0;
   1416   unsigned int n = 0;
   1417 
   1418   while ( (i < len) && (n++ < MAX_ATTRS) )
   1419   {
   1420     const char *name;
   1421     size_t nlen;
   1422     const char *val = NULL;
   1423     size_t vlen = 0;
   1424     char lname[32];
   1425     char lval[256];
   1426 
   1427     while ( (i < len) && (is_space (a[i]) || ('/' == a[i]) ) )
   1428       i++;
   1429     name = &a[i];
   1430     while ( (i < len) && (! is_space (a[i])) &&
   1431             ('=' != a[i]) && ('/' != a[i]) )
   1432       i++;
   1433     nlen = (size_t) (&a[i] - name);
   1434     if (0 == nlen)
   1435       break;
   1436     while ( (i < len) && is_space (a[i]))
   1437       i++;
   1438     if ( (i < len) && ('=' == a[i]) )
   1439     {
   1440       i++;
   1441       while ( (i < len) && is_space (a[i]))
   1442         i++;
   1443       if ( (i < len) && ( ('"' == a[i]) || ('\'' == a[i]) ) )
   1444       {
   1445         const char *q = memchr (&a[i + 1], a[i], len - i - 1);
   1446 
   1447         val = &a[i + 1];
   1448         vlen = (NULL == q) ? len - i - 1 : (size_t) (q - val);
   1449         i += vlen + 2;
   1450       }
   1451       else
   1452       {
   1453         val = &a[i];
   1454         while ( (i < len) && (! is_space (a[i])) )
   1455           i++;
   1456         vlen = (size_t) (&a[i] - val);
   1457       }
   1458     }
   1459     if (nlen >= sizeof (lname))
   1460       continue;
   1461     for (size_t k = 0; k < nlen; k++)
   1462       lname[k] = (char) tolower ((unsigned char) name[k]);
   1463     lname[nlen] = '\0';
   1464     {
   1465       size_t o = 0;
   1466 
   1467       /* lower-case, and drop white space so that `display: none'
   1468          matches */
   1469       for (size_t k = 0; (k < vlen) && (o + 1 < sizeof (lval)); k++)
   1470         if (! is_space (val[k]))
   1471           lval[o++] = (char) tolower ((unsigned char) val[k]);
   1472         else if ( (0 != o) && (' ' != lval[o - 1]) )
   1473           lval[o++] = ' ';
   1474       lval[o] = '\0';
   1475     }
   1476     if (0 == strcmp (lname, "hidden"))
   1477     {
   1478       /* hidden="until-found" is collapsed content that find-in-page
   1479          reveals, i.e. part of the text */
   1480       if (0 != strcmp (lval, "until-found"))
   1481         return 1;
   1482       continue;
   1483     }
   1484     if (0 == strcmp (lname, "aria-hidden"))
   1485     {
   1486       if (0 == strcmp (lval, "true"))
   1487         return 1;
   1488       continue;
   1489     }
   1490     if (0 == strcmp (lname, "style"))
   1491     {
   1492       char css[256];
   1493       size_t o = 0;
   1494 
   1495       for (size_t k = 0; '\0' != lval[k]; k++)
   1496         if (' ' != lval[k])
   1497           css[o++] = lval[k];
   1498       css[o] = '\0';
   1499       if ( (NULL != strstr (css, "display:none")) ||
   1500            (NULL != strstr (css, "visibility:hidden")) )
   1501         return 1;
   1502       continue;
   1503     }
   1504     if ( (0 == strcmp (lname, "role")) ||
   1505          (0 == strcmp (lname, "epub:type")) )
   1506     {
   1507       const char *p = lval;
   1508 
   1509       while ('\0' != *p)
   1510       {
   1511         size_t tl = strcspn (p, " ");
   1512 
   1513         if ( (tl == strlen ("pagebreak")) &&
   1514              (0 == memcmp (p, "pagebreak", tl)) )
   1515           return 1;
   1516         for (unsigned int r = 0; NULL != roles[r]; r++)
   1517           if ( (tl == strlen (roles[r])) &&
   1518                (0 == memcmp (p, roles[r], tl)) )
   1519             return 1;
   1520         p += tl;
   1521         while (' ' == *p)
   1522           p++;
   1523       }
   1524     }
   1525   }
   1526   return 0;
   1527 }
   1528 
   1529 
   1530 /**
   1531  * Handle a <br>: one is a space, two in a row end the paragraph.
   1532  *
   1533  * @param ms conversion state
   1534  */
   1535 static void
   1536 markup_br (struct MarkupState *ms)
   1537 {
   1538   if (ms->after_br)
   1539   {
   1540     EXTRACTOR_fulltext_paragraph_ (ms->ft);
   1541     ms->after_br = 0;
   1542   }
   1543   else
   1544   {
   1545     EXTRACTOR_fulltext_space_ (ms->ft);
   1546     ms->after_br = 1;
   1547   }
   1548 }
   1549 
   1550 
   1551 /**
   1552  * End the current paragraph.
   1553  *
   1554  * @param ms conversion state
   1555  */
   1556 static void
   1557 markup_par (struct MarkupState *ms)
   1558 {
   1559   EXTRACTOR_fulltext_paragraph_ (ms->ft);
   1560   ms->after_br = 0;
   1561 }
   1562 
   1563 
   1564 /**
   1565  * Append character data (between tags) to the text.
   1566  *
   1567  * @param ms conversion state
   1568  * @param p the text, may contain character references
   1569  * @param len number of bytes at @a p
   1570  */
   1571 static void
   1572 markup_chars (struct MarkupState *ms,
   1573               const char *p,
   1574               size_t len)
   1575 {
   1576   size_t i = 0;
   1577 
   1578   while (i < len)
   1579   {
   1580     size_t run = i;
   1581     uint32_t cp;
   1582     size_t used;
   1583 
   1584     while ( (run < len) && ('&' != p[run]) )
   1585     {
   1586       if (ms->after_br && (! is_space (p[run])) )
   1587         ms->after_br = 0;
   1588       run++;
   1589     }
   1590     EXTRACTOR_fulltext_append_ (ms->ft, &p[i], run - i);
   1591     if (run >= len)
   1592       return;
   1593     used = decode_entity (&p[run + 1], len - run - 1, &cp);
   1594     if (0 == used)
   1595     {
   1596       EXTRACTOR_fulltext_append_ (ms->ft, "&", 1);
   1597       i = run + 1;
   1598       ms->after_br = 0;
   1599       continue;
   1600     }
   1601     EXTRACTOR_fulltext_append_cp_ (ms->ft, cp);
   1602     if ( (0xA0 != cp) && (' ' != cp) )
   1603       ms->after_br = 0;
   1604     i = run + 1 + used;
   1605   }
   1606 }
   1607 
   1608 
   1609 /**
   1610  * Find the end of the tag starting at @a p, honouring quoted
   1611  * attribute values.
   1612  *
   1613  * @param p start of the tag body (after the name)
   1614  * @param end end of the buffer
   1615  * @return pointer to the closing '>', or @a end
   1616  */
   1617 static const char *
   1618 tag_end (const char *p,
   1619          const char *end)
   1620 {
   1621   char quote = '\0';
   1622 
   1623   for (; p < end; p++)
   1624   {
   1625     if ('\0' != quote)
   1626     {
   1627       if (*p == quote)
   1628         quote = '\0';
   1629     }
   1630     else if ( ('"' == *p) || ('\'' == *p) )
   1631       quote = *p;
   1632     else if ('>' == *p)
   1633       return p;
   1634   }
   1635   return end;
   1636 }
   1637 
   1638 
   1639 /**
   1640  * Find the case-insensitive end tag `</name' at or after @a p.
   1641  *
   1642  * @param p where to search from
   1643  * @param end end of the buffer
   1644  * @param name lower-case element name
   1645  * @return pointer to the '<' of the end tag, or @a end
   1646  */
   1647 static const char *
   1648 find_end_tag (const char *p,
   1649               const char *end,
   1650               const char *name)
   1651 {
   1652   size_t nlen = strlen (name);
   1653 
   1654   for (; p + 2 + nlen <= end; p++)
   1655   {
   1656     if ( ('<' != p[0]) || ('/' != p[1]) )
   1657       continue;
   1658     if (0 == strncasecmp (&p[2], name, nlen))
   1659       return p;
   1660   }
   1661   return end;
   1662 }
   1663 
   1664 
   1665 /**
   1666  * Convert one (X)HTML document to text.  This is a tolerant scanner,
   1667  * not a parser: EPUB content documents are XHTML, but MOBI text is
   1668  * HTML 3.2 with unclosed elements, so all we track is which element's
   1669  * content we are skipping and whether we are inside a sectioning
   1670  * element.  Block-level elements end paragraphs, as do MOBI page
   1671  * breaks (<mbp:pagebreak/>); one <br> is a space and two end a
   1672  * paragraph.  Memory and time are linear in @a len; there is no
   1673  * recursion.
   1674  *
   1675  * @param ft where the text goes
   1676  * @param data the document in UTF-8
   1677  * @param len number of bytes in @a data
   1678  */
   1679 static void
   1680 markup_to_text (struct EXTRACTOR_FullText *ft,
   1681                 const char *data,
   1682                 size_t len)
   1683 {
   1684   struct MarkupState ms;
   1685   const char *p = data;
   1686   const char *end = data + len;
   1687 
   1688   memset (&ms, 0, sizeof (ms));
   1689   ms.ft = ft;
   1690   while ( (p < end) && (! ft->full) )
   1691   {
   1692     const char *lt;
   1693     const char *name;
   1694     const char *gt;
   1695     char local[32];
   1696     size_t nlen;
   1697     int closing;
   1698     int self_closing;
   1699 
   1700     lt = memchr (p, '<', (size_t) (end - p));
   1701     if (NULL == lt)
   1702       lt = end;
   1703     if ('\0' == ms.skip_name[0])
   1704       markup_chars (&ms, p, (size_t) (lt - p));
   1705     p = lt;
   1706     if (p >= end)
   1707       break;
   1708     if ( (end - p >= 4) && (0 == memcmp (p, "<!--", 4)) )
   1709     {
   1710       const char *c = mem_find (p + 4, (size_t) (end - p - 4), "-->", 3);
   1711 
   1712       p = (NULL == c) ? end : c + 3;
   1713       continue;
   1714     }
   1715     if ( (end - p >= 9) && (0 == memcmp (p, "<![CDATA[", 9)) )
   1716     {
   1717       const char *c = mem_find (p + 9, (size_t) (end - p - 9), "]]>", 3);
   1718       const char *ce = (NULL == c) ? end : c;
   1719 
   1720       if ('\0' == ms.skip_name[0])
   1721       {
   1722         EXTRACTOR_fulltext_append_ (ft, p + 9, (size_t) (ce - p - 9));
   1723         ms.after_br = 0;
   1724       }
   1725       p = (NULL == c) ? end : c + 3;
   1726       continue;
   1727     }
   1728     if ( (end - p >= 2) && ( ('!' == p[1]) || ('?' == p[1]) ) )
   1729     {
   1730       gt = memchr (p, '>', (size_t) (end - p));
   1731       p = (NULL == gt) ? end : gt + 1;
   1732       continue;
   1733     }
   1734     closing = ( (end - p >= 2) && ('/' == p[1]) );
   1735     name = p + (closing ? 2 : 1);
   1736     nlen = 0;
   1737     while ( (name + nlen < end) &&
   1738             is_name_char (name[nlen]) )
   1739       nlen++;
   1740     if ( (0 == nlen) ||
   1741          (! isalpha ((unsigned char) name[0])) )
   1742     {
   1743       /* a stray '<' in the text */
   1744       if ('\0' == ms.skip_name[0])
   1745         markup_chars (&ms, "<", 1);
   1746       p++;
   1747       continue;
   1748     }
   1749     gt = tag_end (name + nlen, end);
   1750     self_closing = ( (! closing) && (gt > name + nlen) && ('/' == gt[-1]) );
   1751     {
   1752       /* the local name, lower case, without namespace prefix */
   1753       const char *l = name;
   1754       size_t ll;
   1755 
   1756       for (size_t k = 0; k < nlen; k++)
   1757         if (':' == name[k])
   1758           l = &name[k + 1];
   1759       ll = (size_t) (name + nlen - l);
   1760       if (ll >= sizeof (local))
   1761         ll = sizeof (local) - 1;
   1762       for (size_t k = 0; k < ll; k++)
   1763         local[k] = (char) tolower ((unsigned char) l[k]);
   1764       local[ll] = '\0';
   1765     }
   1766     p = (gt < end) ? gt + 1 : end;
   1767 
   1768     if ( (! closing) && (! self_closing) &&
   1769          name_in (local, raw_text_elements) )
   1770     {
   1771       /* no markup inside; the content is never text, except for the
   1772          obsolete <xmp>, which is rare enough to skip too */
   1773       const char *et = find_end_tag (p, end, local);
   1774 
   1775       if (et < end)
   1776         et = tag_end (et, end);
   1777       p = (et < end) ? et + 1 : end;
   1778       if ('\0' == ms.skip_name[0])
   1779         markup_par (&ms);
   1780       continue;
   1781     }
   1782     if ('\0' != ms.skip_name[0])
   1783     {
   1784       if (0 != strcmp (local, ms.skip_name))
   1785         continue;
   1786       if (closing)
   1787       {
   1788         if (0 == --ms.skip_nest)
   1789         {
   1790           ms.skip_name[0] = '\0';
   1791           if (name_in (local, block_elements))
   1792             markup_par (&ms);
   1793         }
   1794       }
   1795       else if (! self_closing)
   1796       {
   1797         ms.skip_nest++;
   1798       }
   1799       continue;
   1800     }
   1801     if (closing)
   1802     {
   1803       if (name_in (local, sectioning_elements) && (ms.sections > 0))
   1804         ms.sections--;
   1805       if (name_in (local, block_elements))
   1806         markup_par (&ms);
   1807       continue;
   1808     }
   1809     if (name_in (local, skip_elements) ||
   1810         ( ( (0 == strcmp (local, "header")) ||
   1811             (0 == strcmp (local, "footer")) ) &&
   1812           (0 == ms.sections) ) ||
   1813         attrs_skip (name + nlen, (size_t) (gt - name - nlen)) )
   1814     {
   1815       if (name_in (local, block_elements))
   1816         markup_par (&ms);
   1817       if ( (! self_closing) &&
   1818            (! name_in (local, void_elements)) )
   1819       {
   1820         strcpy (ms.skip_name, local);
   1821         ms.skip_nest = 1;
   1822       }
   1823       continue;
   1824     }
   1825     if (0 == strcmp (local, "br"))
   1826     {
   1827       markup_br (&ms);
   1828       continue;
   1829     }
   1830     if ( (0 == strcmp (local, "pagebreak")) ||
   1831          name_in (local, block_elements) )
   1832       markup_par (&ms);
   1833     if ( (! self_closing) &&
   1834          name_in (local, sectioning_elements) )
   1835       ms.sections++;
   1836   }
   1837 }
   1838 
   1839 
   1840 /* ------------------------------------------------------------------ */
   1841 /* Full text: EPUB                                                     */
   1842 /* ------------------------------------------------------------------ */
   1843 
   1844 /**
   1845  * One entry of the OPF manifest.
   1846  */
   1847 struct ManifestItem
   1848 {
   1849   /**
   1850    * The item's id.
   1851    */
   1852   char *id;
   1853 
   1854   /**
   1855    * The item's href, relative to the OPF.
   1856    */
   1857   char *href;
   1858 };
   1859 
   1860 
   1861 /**
   1862  * Find the next start tag named @a name (not a longer name that begins
   1863  * with it) in @a data from offset @a *pos.
   1864  *
   1865  * @param data buffer to search
   1866  * @param len number of bytes in @a data
   1867  * @param[in,out] pos where to start; set to just past the tag
   1868  * @param name element name including the '<', e.g. "<item"
   1869  * @param[out] body start of the attributes
   1870  * @param[out] body_len number of bytes of attributes
   1871  * @return 1 if found
   1872  */
   1873 static int
   1874 next_tag (const char *data,
   1875           size_t len,
   1876           size_t *pos,
   1877           const char *name,
   1878           const char **body,
   1879           size_t *body_len)
   1880 {
   1881   size_t nlen = strlen (name);
   1882 
   1883   while (*pos < len)
   1884   {
   1885     const char *t = mem_find (&data[*pos], len - *pos, name, nlen);
   1886     const char *gt;
   1887     size_t off;
   1888 
   1889     if (NULL == t)
   1890     {
   1891       *pos = len;
   1892       return 0;
   1893     }
   1894     off = (size_t) (t - data) + nlen;
   1895     if ( (off < len) &&
   1896          is_name_char (data[off]) )
   1897     {
   1898       *pos = off;
   1899       continue;
   1900     }
   1901     gt = tag_end (&data[off], data + len);
   1902     *body = &data[off];
   1903     *body_len = (size_t) (gt - &data[off]);
   1904     *pos = (size_t) (gt - data) + ( (gt < data + len) ? 1 : 0);
   1905     return 1;
   1906   }
   1907   return 0;
   1908 }
   1909 
   1910 
   1911 /**
   1912  * Value of a hex digit.
   1913  *
   1914  * @param c character
   1915  * @return value, or -1 if @a c is not a hex digit
   1916  */
   1917 static int
   1918 hex_val (char c)
   1919 {
   1920   if ( ('0' <= c) && ('9' >= c) )
   1921     return c - '0';
   1922   if ( ('a' <= c) && ('f' >= c) )
   1923     return c - 'a' + 10;
   1924   if ( ('A' <= c) && ('F' >= c) )
   1925     return c - 'A' + 10;
   1926   return -1;
   1927 }
   1928 
   1929 
   1930 /**
   1931  * Resolve @a href relative to the directory of @a base (a path inside
   1932  * the container): drop the fragment, undo percent-encoding and
   1933  * normalise `.' and `..' segments.
   1934  *
   1935  * @param base path of the document @a href appears in
   1936  * @param href the reference
   1937  * @param[out] out where to write the member name
   1938  * @param outsz number of bytes available in @a out
   1939  * @return 1 on success, 0 if @a href is not a local path we can use
   1940  */
   1941 static int
   1942 resolve_href (const char *base,
   1943               const char *href,
   1944               char *out,
   1945               size_t outsz)
   1946 {
   1947   char tmp[1024];
   1948   size_t t = 0;
   1949   size_t o = 0;
   1950   const char *slash;
   1951   const char *seg;
   1952 
   1953   if ('\0' == href[0])
   1954     return 0;
   1955   /* a scheme (http:, data:, ...) means the resource is not in the
   1956      container */
   1957   for (size_t i = 0; '\0' != href[i]; i++)
   1958   {
   1959     if (':' == href[i])
   1960       return 0;
   1961     if ( ('/' == href[i]) || ('#' == href[i]) || ('?' == href[i]) )
   1962       break;
   1963   }
   1964   if ('/' != href[0])
   1965   {
   1966     slash = strrchr (base, '/');
   1967     if (NULL != slash)
   1968     {
   1969       size_t bl = (size_t) (slash - base) + 1;
   1970 
   1971       if (bl >= sizeof (tmp))
   1972         return 0;
   1973       memcpy (tmp, base, bl);
   1974       t = bl;
   1975     }
   1976   }
   1977   for (size_t i = 0; ('\0' != href[i]) && ('#' != href[i]) &&
   1978        ('?' != href[i]); i++)
   1979   {
   1980     char c = href[i];
   1981 
   1982     if ( ('%' == c) &&
   1983          (hex_val (href[i + 1]) >= 0) &&
   1984          (hex_val (href[i + 2]) >= 0) )
   1985     {
   1986       c = (char) (hex_val (href[i + 1]) * 16 + hex_val (href[i + 2]));
   1987       i += 2;
   1988     }
   1989     if ('\0' == c)
   1990       return 0;
   1991     if (t + 1 >= sizeof (tmp))
   1992       return 0;
   1993     tmp[t++] = c;
   1994   }
   1995   tmp[t] = '\0';
   1996   /* normalise the segments into out */
   1997   seg = tmp;
   1998   while ('\0' != *seg)
   1999   {
   2000     size_t sl = strcspn (seg, "/");
   2001 
   2002     if ( (0 == sl) ||
   2003          ( (1 == sl) && ('.' == seg[0]) ) )
   2004     {
   2005       /* empty or `.' */
   2006     }
   2007     else if ( (2 == sl) && ('.' == seg[0]) && ('.' == seg[1]) )
   2008     {
   2009       /* `..': drop the last segment of out */
   2010       while ( (o > 0) && ('/' != out[o - 1]) )
   2011         o--;
   2012       if (o > 0)
   2013         o--;
   2014       while ( (o > 0) && ('/' != out[o - 1]) )
   2015         o--;
   2016     }
   2017     else
   2018     {
   2019       if (o + sl + 2 > outsz)
   2020         return 0;
   2021       memcpy (&out[o], seg, sl);
   2022       o += sl;
   2023       out[o++] = '/';
   2024     }
   2025     seg += sl;
   2026     if ('/' == *seg)
   2027       seg++;
   2028   }
   2029   if (0 == o)
   2030     return 0;
   2031   out[o - 1] = '\0';   /* replace the trailing '/' */
   2032   return 1;
   2033 }
   2034 
   2035 
   2036 /**
   2037  * Release a manifest.
   2038  *
   2039  * @param items the manifest
   2040  * @param n number of entries in @a items
   2041  */
   2042 static void
   2043 free_manifest (struct ManifestItem *items,
   2044                unsigned int n)
   2045 {
   2046   for (unsigned int i = 0; i < n; i++)
   2047   {
   2048     free (items[i].id);
   2049     free (items[i].href);
   2050   }
   2051   free (items);
   2052 }
   2053 
   2054 
   2055 /**
   2056  * One content document in the spine.
   2057  */
   2058 struct SpineDoc
   2059 {
   2060   /**
   2061    * Member name in the archive.
   2062    */
   2063   char *member;
   2064 
   2065   /**
   2066    * The document's text (normalized as by #EXTRACTOR_FullText), NULL
   2067    * if not (yet) read.  Shared between entries for the same member.
   2068    */
   2069   char *text;
   2070 
   2071   /**
   2072    * Number of bytes in @e text.
   2073    */
   2074   size_t text_len;
   2075 
   2076   /**
   2077    * 1 if this entry owns @e text.
   2078    */
   2079   int owner;
   2080 };
   2081 
   2082 
   2083 /**
   2084  * Index entry for looking up spine documents by member name.
   2085  */
   2086 struct SpineKey
   2087 {
   2088   /**
   2089    * Member name (owned by the `struct SpineDoc`).
   2090    */
   2091   const char *member;
   2092 
   2093   /**
   2094    * Position in the spine.
   2095    */
   2096   unsigned int idx;
   2097 };
   2098 
   2099 
   2100 /**
   2101  * Order spine keys by member name (ignoring case, as the archive
   2102  * lookup does), then by spine position.
   2103  *
   2104  * @param a a `struct SpineKey`
   2105  * @param b a `struct SpineKey`
   2106  * @return comparison result
   2107  */
   2108 static int
   2109 cmp_spine_key (const void *a,
   2110                const void *b)
   2111 {
   2112   const struct SpineKey *ka = a;
   2113   const struct SpineKey *kb = b;
   2114   int r = strcasecmp (ka->member,
   2115                       kb->member);
   2116 
   2117   if (0 != r)
   2118     return r;
   2119   return (ka->idx < kb->idx) ? -1 : (ka->idx > kb->idx);
   2120 }
   2121 
   2122 
   2123 /**
   2124  * Parse the manifest and spine of the OPF: the content documents in
   2125  * reading order.
   2126  *
   2127  * @param opf the package document
   2128  * @param opf_len number of bytes in @a opf
   2129  * @param opf_path member name of the package document
   2130  * @param enc contents of META-INF/encryption.xml, or NULL
   2131  * @param enc_len number of bytes in @a enc
   2132  * @param[out] docs the spine, caller frees (also the member names)
   2133  * @return number of entries in @a docs
   2134  */
   2135 static unsigned int
   2136 parse_spine (const char *opf,
   2137              size_t opf_len,
   2138              const char *opf_path,
   2139              const char *enc,
   2140              size_t enc_len,
   2141              struct SpineDoc **docs)
   2142 {
   2143   struct ManifestItem *items;
   2144   struct SpineDoc *d;
   2145   unsigned int n_items = 0;
   2146   unsigned int n_docs = 0;
   2147   unsigned int n_refs = 0;
   2148   size_t pos;
   2149   const char *body;
   2150   size_t body_len;
   2151   const char *spine;
   2152   const char *spine_end;
   2153 
   2154   *docs = NULL;
   2155   spine = mem_find (opf, opf_len, "<spine", strlen ("<spine"));
   2156   if (NULL == spine)
   2157     return 0;
   2158   spine_end = mem_find (spine,
   2159                         opf_len - (size_t) (spine - opf),
   2160                         "</spine",
   2161                         strlen ("</spine"));
   2162   if (NULL == spine_end)
   2163     spine_end = opf + opf_len;
   2164   items = calloc (MAX_MANIFEST, sizeof (struct ManifestItem));
   2165   d = calloc (MAX_SPINE, sizeof (struct SpineDoc));
   2166   if ( (NULL == items) ||
   2167        (NULL == d) )
   2168   {
   2169     free (items);
   2170     free (d);
   2171     return 0;
   2172   }
   2173   pos = 0;
   2174   while ( (n_items < MAX_MANIFEST) &&
   2175           next_tag (opf, opf_len, &pos, "<item", &body, &body_len) )
   2176   {
   2177     char id[256];
   2178     char href[1024];
   2179     char mt[128];
   2180 
   2181     if ( (! xml_attr (body, body_len, "id", id, sizeof (id))) ||
   2182          (! xml_attr (body, body_len, "href", href, sizeof (href))) )
   2183       continue;
   2184     if (! xml_attr (body, body_len, "media-type", mt, sizeof (mt)))
   2185       mt[0] = '\0';
   2186     if ( (NULL == strstr (mt, "html")) &&
   2187          (NULL == strstr (mt, "oeb1-document")) &&
   2188          (NULL == strstr (mt, "dtbook")) )
   2189       continue;   /* images, style sheets, fonts, the NCX */
   2190     items[n_items].id = strdup (id);
   2191     items[n_items].href = strdup (href);
   2192     if ( (NULL == items[n_items].id) ||
   2193          (NULL == items[n_items].href) )
   2194     {
   2195       free (items[n_items].id);
   2196       free (items[n_items].href);
   2197       break;
   2198     }
   2199     n_items++;
   2200   }
   2201   pos = 0;
   2202   while ( (n_refs++ < MAX_SPINE) &&
   2203           next_tag (spine,
   2204                     (size_t) (spine_end - spine),
   2205                     &pos,
   2206                     "<itemref",
   2207                     &body,
   2208                     &body_len) )
   2209   {
   2210     char idref[256];
   2211     char linear[16];
   2212     char member[1024];
   2213     unsigned int i;
   2214 
   2215     if (! xml_attr (body, body_len, "idref", idref, sizeof (idref)))
   2216       continue;
   2217     /* linear="no" marks auxiliary content (pop-up notes, answer keys)
   2218        that is outside the reading order */
   2219     if ( (xml_attr (body, body_len, "linear", linear, sizeof (linear))) &&
   2220          (0 == strcmp (linear, "no")) )
   2221       continue;
   2222     for (i = 0; i < n_items; i++)
   2223       if (0 == strcmp (items[i].id, idref))
   2224         break;
   2225     if (i == n_items)
   2226       continue;
   2227     if (! resolve_href (opf_path,
   2228                         items[i].href,
   2229                         member,
   2230                         sizeof (member)))
   2231       continue;
   2232     /* content documents that are listed in encryption.xml are DRM
   2233        protected; font obfuscation only lists fonts */
   2234     if ( (NULL != enc) &&
   2235          (NULL != mem_find (enc, enc_len, member, strlen (member))) )
   2236       continue;
   2237     if (NULL == (d[n_docs].member = strdup (member)))
   2238       break;
   2239     n_docs++;
   2240   }
   2241   free_manifest (items, n_items);
   2242   *docs = d;
   2243   return n_docs;
   2244 }
   2245 
   2246 
   2247 /**
   2248  * Append @a text, which is normalized as by #EXTRACTOR_FullText
   2249  * (paragraphs separated by "\n\n"), to @a ft.
   2250  *
   2251  * @param ft where to append
   2252  * @param text the text
   2253  * @param len number of bytes in @a text
   2254  */
   2255 static void
   2256 append_normalized (struct EXTRACTOR_FullText *ft,
   2257                    const char *text,
   2258                    size_t len)
   2259 {
   2260   size_t i = 0;
   2261 
   2262   while ( (i < len) &&
   2263           (! ft->full) )
   2264   {
   2265     const char *nl = memchr (&text[i], '\n', len - i);
   2266     size_t pl = (NULL == nl) ? len - i : (size_t) (nl - &text[i]);
   2267 
   2268     EXTRACTOR_fulltext_append_ (ft, &text[i], pl);
   2269     EXTRACTOR_fulltext_paragraph_ (ft);
   2270     i += pl;
   2271     while ( (i < len) && ('\n' == text[i]) )
   2272       i++;
   2273   }
   2274 }
   2275 
   2276 
   2277 /**
   2278  * Collect the text of an EPUB in reading order (the linear entries of
   2279  * the OPF spine) and emit it.
   2280  *
   2281  * Looking a member up by name walks the central directory, so doing
   2282  * that once per spine entry would be quadratic in a hostile archive.
   2283  * Instead, the archive is walked once, each content document found in
   2284  * the spine is converted to text as it comes along, and the texts are
   2285  * put in spine order at the end.
   2286  *
   2287  * @param ec extraction context
   2288  * @param uf the archive
   2289  * @param opf the package document
   2290  * @param opf_len number of bytes in @a opf
   2291  * @param opf_path member name of the package document
   2292  * @return 1 if the caller should stop extracting, 0 to continue
   2293  */
   2294 static int
   2295 epub_fulltext (struct EXTRACTOR_ExtractContext *ec,
   2296                struct EXTRACTOR_UnzipFile *uf,
   2297                const char *opf,
   2298                size_t opf_len,
   2299                const char *opf_path)
   2300 {
   2301   struct EXTRACTOR_FullText ft;
   2302   struct SpineDoc *docs;
   2303   struct SpineKey *order;
   2304   unsigned int n_docs;
   2305   size_t budget = MAX_BOOK_MARKUP;
   2306   size_t text_total = 0;
   2307   size_t enc_len = 0;
   2308   char *enc;
   2309   int ret;
   2310 
   2311   enc = read_member (uf,
   2312                      "META-INF/encryption.xml",
   2313                      MAX_OPF,
   2314                      &enc_len);
   2315   n_docs = parse_spine (opf,
   2316                         opf_len,
   2317                         opf_path,
   2318                         enc,
   2319                         enc_len,
   2320                         &docs);
   2321   free (enc);
   2322   if (0 == n_docs)
   2323   {
   2324     free (docs);
   2325     return 0;
   2326   }
   2327   if (NULL == (order = malloc (n_docs * sizeof (struct SpineKey))))
   2328   {
   2329     for (unsigned int i = 0; i < n_docs; i++)
   2330       free (docs[i].member);
   2331     free (docs);
   2332     return 0;
   2333   }
   2334   for (unsigned int i = 0; i < n_docs; i++)
   2335   {
   2336     order[i].member = docs[i].member;
   2337     order[i].idx = i;
   2338   }
   2339   qsort (order, n_docs, sizeof (struct SpineKey), &cmp_spine_key);
   2340 
   2341   if (EXTRACTOR_UNZIP_OK ==
   2342       EXTRACTOR_common_unzip_go_to_first_file (uf))
   2343   {
   2344     do
   2345     {
   2346       struct EXTRACTOR_UnzipFileInfo fi;
   2347       struct EXTRACTOR_FullText cft;
   2348       char name[1024];
   2349       unsigned int lo = 0;
   2350       unsigned int hi = n_docs;
   2351       unsigned int first;
   2352       char *doc;
   2353       char *utf8;
   2354       size_t doc_len;
   2355       size_t utf8_len;
   2356 
   2357       if (EXTRACTOR_UNZIP_OK !=
   2358           EXTRACTOR_common_unzip_get_current_file_info (uf,
   2359                                                         &fi,
   2360                                                         name,
   2361                                                         sizeof (name),
   2362                                                         NULL, 0,
   2363                                                         NULL, 0))
   2364         break;
   2365       name[sizeof (name) - 1] = '\0';
   2366       /* first spine entry for this member (binary search) */
   2367       while (lo < hi)
   2368       {
   2369         unsigned int mid = lo + (hi - lo) / 2;
   2370 
   2371         if (strcasecmp (order[mid].member, name) < 0)
   2372           lo = mid + 1;
   2373         else
   2374           hi = mid;
   2375       }
   2376       if ( (lo >= n_docs) ||
   2377            (0 != strcasecmp (order[lo].member, name)) )
   2378         continue;
   2379       first = order[lo].idx;
   2380       if (NULL != docs[first].text)
   2381         continue;   /* the same name twice in the archive */
   2382       doc = read_current (uf,
   2383                           (budget < MAX_CHAPTER) ? budget : MAX_CHAPTER,
   2384                           &doc_len);
   2385       if (NULL == doc)
   2386         continue;
   2387       budget -= doc_len;
   2388       utf8 = xhtml_to_utf8 (doc, doc_len, &utf8_len);
   2389       free (doc);
   2390       if (NULL == utf8)
   2391         continue;
   2392       EXTRACTOR_fulltext_init_ (&cft);
   2393       EXTRACTOR_fulltext_set_context_ (&cft, ec);
   2394       markup_to_text (&cft, utf8, utf8_len);
   2395       free (utf8);
   2396       if (0 == cft.len)
   2397       {
   2398         EXTRACTOR_fulltext_clear_ (&cft);
   2399         continue;
   2400       }
   2401       text_total += cft.len;
   2402       /* every spine entry for this member shares the text */
   2403       for (unsigned int k = lo;
   2404            (k < n_docs) &&
   2405            (0 == strcasecmp (order[k].member, name));
   2406            k++)
   2407       {
   2408         docs[order[k].idx].text = cft.buf;
   2409         docs[order[k].idx].text_len = cft.len;
   2410       }
   2411       docs[first].owner = 1;
   2412     }
   2413     while ( (budget > 0) &&
   2414             (text_total < EXTRACTOR_FULLTEXT_MAX) &&
   2415             (EXTRACTOR_UNZIP_OK ==
   2416              EXTRACTOR_common_unzip_go_to_next_file (uf)) );
   2417   }
   2418   free (order);
   2419 
   2420   EXTRACTOR_fulltext_init_ (&ft);
   2421   EXTRACTOR_fulltext_set_context_ (&ft, ec);
   2422   for (unsigned int i = 0; (i < n_docs) && (! ft.full); i++)
   2423   {
   2424     /* each spine entry is a chapter, which always ends a paragraph */
   2425     if (NULL != docs[i].text)
   2426       append_normalized (&ft, docs[i].text, docs[i].text_len);
   2427     EXTRACTOR_fulltext_paragraph_ (&ft);
   2428   }
   2429   for (unsigned int i = 0; i < n_docs; i++)
   2430   {
   2431     if (docs[i].owner)
   2432       free (docs[i].text);
   2433     free (docs[i].member);
   2434   }
   2435   free (docs);
   2436   ret = EXTRACTOR_fulltext_emit_ (&ft, ec, PLUGIN_NAME);
   2437   EXTRACTOR_fulltext_clear_ (&ft);
   2438   return (0 != ret) ? 1 : 0;
   2439 }
   2440 
   2441 
   2442 /**
   2443  * Handle an EPUB: read `META-INF/container.xml' to find the OPF package
   2444  * document, then report what the package document holds.
   2445  *
   2446  * @param ec extraction context
   2447  * @param uf the opened archive
   2448  */
   2449 static void
   2450 extract_epub (struct EXTRACTOR_ExtractContext *ec,
   2451               struct EXTRACTOR_UnzipFile *uf)
   2452 {
   2453   char path[512];
   2454   char opf_path[512];
   2455   char attr[64];
   2456   char *container;
   2457   char *opf = NULL;
   2458   size_t container_len;
   2459   size_t opf_len = 0;
   2460   const char *p;
   2461   const char *q;
   2462   unsigned int items = 0;
   2463 
   2464   if (0 !=
   2465       ec->proc (ec->cls,
   2466                 PLUGIN_NAME,
   2467                 EXTRACTOR_METATYPE_MIMETYPE,
   2468                 EXTRACTOR_METAFORMAT_UTF8,
   2469                 "text/plain",
   2470                 "application/epub+zip",
   2471                 strlen ("application/epub+zip") + 1))
   2472     return;
   2473   container = read_member (uf,
   2474                            "META-INF/container.xml",
   2475                            MAX_CONTAINER,
   2476                            &container_len);
   2477   if (NULL != container)
   2478   {
   2479     p = mem_find (container,
   2480                   container_len,
   2481                   "full-path=",
   2482                   strlen ("full-path="));
   2483     if (NULL != p)
   2484     {
   2485       p += strlen ("full-path=");
   2486       if ( ('"' == *p) || ('\'' == *p) )
   2487       {
   2488         char quote = *p;
   2489 
   2490         p++;
   2491         q = memchr (p,
   2492                     quote,
   2493                     container_len - (size_t) (p - container));
   2494         if (NULL != q)
   2495           (void) xml_text (p,
   2496                            (size_t) (q - p),
   2497                            path,
   2498                            sizeof (path));
   2499         else
   2500           path[0] = '\0';
   2501       }
   2502       else
   2503       {
   2504         path[0] = '\0';
   2505       }
   2506       if ('\0' != path[0])
   2507       {
   2508         opf = read_member (uf,
   2509                            path,
   2510                            MAX_OPF,
   2511                            &opf_len);
   2512         if (NULL != opf)
   2513           memcpy (opf_path, path, sizeof (opf_path));
   2514       }
   2515     }
   2516     free (container);
   2517   }
   2518   if (NULL == opf)
   2519   {
   2520     /* no usable container.xml; the conventional locations are worth a
   2521        try before giving up */
   2522     static const char *guesses[] = {
   2523       "OEBPS/content.opf",
   2524       "content.opf",
   2525       "OPS/content.opf",
   2526       NULL
   2527     };
   2528 
   2529     for (unsigned int i = 0; NULL != guesses[i]; i++)
   2530     {
   2531       opf = read_member (uf,
   2532                          guesses[i],
   2533                          MAX_OPF,
   2534                          &opf_len);
   2535       if (NULL != opf)
   2536       {
   2537         snprintf (opf_path, sizeof (opf_path), "%s", guesses[i]);
   2538         break;
   2539       }
   2540     }
   2541   }
   2542   if (NULL != opf)
   2543   {
   2544     /* the <package version="..."> attribute distinguishes EPUB 2 from
   2545        EPUB 3, which changes what else we may expect to find */
   2546     p = mem_find (opf,
   2547                   opf_len,
   2548                   "<package",
   2549                   strlen ("<package"));
   2550     if (NULL != p)
   2551     {
   2552       size_t tail = opf_len - (size_t) (p - opf);
   2553       const char *gt;
   2554 
   2555       if (tail > 1024)
   2556         tail = 1024;
   2557       /* stop at the end of the start tag, so that a `version=' further
   2558          down the document cannot be mistaken for this one */
   2559       gt = memchr (p,
   2560                    '>',
   2561                    tail);
   2562       if (NULL != gt)
   2563         tail = (size_t) (gt - p);
   2564       if (tail <= strlen ("<package"))
   2565         tail = strlen ("<package");
   2566       if (xml_attr (p + strlen ("<package"),
   2567                     tail - strlen ("<package"),
   2568                     "version",
   2569                     attr,
   2570                     sizeof (attr)))
   2571       {
   2572         if (0 !=
   2573             EXTRACTOR_forensic_emit_text_ (ec,
   2574                                            PLUGIN_NAME,
   2575                                            EXTRACTOR_METATYPE_FORMAT_VERSION,
   2576                                            attr,
   2577                                            strlen (attr)))
   2578           goto CLEANUP;
   2579       }
   2580     }
   2581     if (0 !=
   2582         parse_opf_metadata (ec,
   2583                             opf,
   2584                             opf_len))
   2585       goto CLEANUP;
   2586     /* count the manifest entries; `<itemref>' does not match because
   2587        the character after `<item' has to be a name delimiter */
   2588     for (size_t i = 0; i + strlen ("<item") < opf_len; i++)
   2589       if ( (0 == memcmp (&opf[i],
   2590                          "<item",
   2591                          strlen ("<item"))) &&
   2592            (! is_name_char (opf[i + strlen ("<item")])) )
   2593         items++;
   2594     if (0 != items)
   2595     {
   2596       if (0 !=
   2597           EXTRACTOR_forensic_emit_ (ec,
   2598                                     PLUGIN_NAME,
   2599                                     EXTRACTOR_METATYPE_ENTRY_COUNT,
   2600                                     "%u",
   2601                                     items))
   2602         goto CLEANUP;
   2603     }
   2604   }
   2605   if (has_member (uf,
   2606                   "META-INF/encryption.xml"))
   2607   {
   2608     if (0 !=
   2609         EXTRACTOR_forensic_emit_ (
   2610           ec,
   2611           PLUGIN_NAME,
   2612           EXTRACTOR_METATYPE_ENCRYPTION,
   2613           "META-INF/encryption.xml present"
   2614           " (DRM or font obfuscation)"))
   2615       goto CLEANUP;
   2616   }
   2617   if (has_member (uf,
   2618                   "META-INF/rights.xml"))
   2619   {
   2620     if (0 !=
   2621         EXTRACTOR_forensic_emit_ (ec,
   2622                                   PLUGIN_NAME,
   2623                                   EXTRACTOR_METATYPE_ENCRYPTION,
   2624                                   "META-INF/rights.xml present"
   2625                                   " (Adobe Content Server DRM)"))
   2626       goto CLEANUP;
   2627   }
   2628   if ( (NULL != opf) &&
   2629        (EXTRACTOR_fulltext_requested_ (ec->config)) )
   2630     (void) epub_fulltext (ec,
   2631                           uf,
   2632                           opf,
   2633                           opf_len,
   2634                           opf_path);
   2635 CLEANUP:
   2636   free (opf);
   2637 }
   2638 
   2639 
   2640 /* ------------------------------------------------------------------ */
   2641 /* Full text: MOBI                                                     */
   2642 /* ------------------------------------------------------------------ */
   2643 
   2644 /**
   2645  * Size of the trailing entries at the end of a MOBI text record, as
   2646  * announced by the "extra data flags" of the MOBI header: bit 0 is the
   2647  * multibyte-character overlap (its size is in the low two bits of the
   2648  * last byte before the other entries), every other bit is an entry
   2649  * whose size is a backward-encoded variable-length integer at its end.
   2650  *
   2651  * @param d the record
   2652  * @param size number of bytes in @a d
   2653  * @param flags extra data flags
   2654  * @return number of bytes to strip from the end of the record
   2655  */
   2656 static size_t
   2657 trailing_size (const unsigned char *d,
   2658                size_t size,
   2659                uint16_t flags)
   2660 {
   2661   size_t num = 0;
   2662 
   2663   for (unsigned int bit = 1; bit < 16; bit++)
   2664   {
   2665     size_t end;
   2666     uint32_t v = 0;
   2667 
   2668     if (0 == (flags & (1u << bit)))
   2669       continue;
   2670     end = size - num;
   2671     if (0 == end)
   2672       return size;
   2673     for (size_t k = (end >= 4) ? end - 4 : 0; k < end; k++)
   2674     {
   2675       if (0 != (d[k] & 0x80))
   2676         v = 0;
   2677       v = (v << 7) | (d[k] & 0x7F);
   2678     }
   2679     if (v > end)
   2680       return size;
   2681     num += v;
   2682   }
   2683   if (0 != (flags & 1))
   2684   {
   2685     if (num >= size)
   2686       return size;
   2687     num += (d[size - num - 1] & 0x3) + 1;
   2688   }
   2689   return (num > size) ? size : num;
   2690 }
   2691 
   2692 
   2693 /**
   2694  * Decompress one PalmDOC (LZ77 variant) compressed record.
   2695  *
   2696  * @param in compressed data
   2697  * @param len number of bytes in @a in
   2698  * @param[out] out where to write the text
   2699  * @param outsz number of bytes available in @a out
   2700  * @return number of bytes written
   2701  */
   2702 static size_t
   2703 palmdoc_decompress (const unsigned char *in,
   2704                     size_t len,
   2705                     unsigned char *out,
   2706                     size_t outsz)
   2707 {
   2708   size_t i = 0;
   2709   size_t o = 0;
   2710 
   2711   while ( (i < len) && (o < outsz) )
   2712   {
   2713     unsigned int c = in[i++];
   2714 
   2715     if ( (0 == c) ||
   2716          ( (c >= 0x09) && (c <= 0x7F) ) )
   2717     {
   2718       out[o++] = (unsigned char) c;
   2719     }
   2720     else if (c <= 0x08)
   2721     {
   2722       /* c literal bytes follow */
   2723       for (unsigned int k = 0; (k < c) && (i < len) && (o < outsz); k++)
   2724         out[o++] = in[i++];
   2725     }
   2726     else if (c <= 0xBF)
   2727     {
   2728       unsigned int pair;
   2729       size_t dist;
   2730       unsigned int n;
   2731 
   2732       if (i >= len)
   2733         break;
   2734       pair = ((c << 8) | in[i++]) & 0x3FFF;
   2735       dist = pair >> 3;
   2736       n = (pair & 7) + 3;
   2737       if ( (0 == dist) ||
   2738            (dist > o) )
   2739         break;   /* corrupt: points before the start */
   2740       for (unsigned int k = 0; (k < n) && (o < outsz); k++, o++)
   2741         out[o] = out[o - dist];
   2742     }
   2743     else
   2744     {
   2745       /* a space followed by the character c ^ 0x80 */
   2746       out[o++] = ' ';
   2747       if (o < outsz)
   2748         out[o++] = (unsigned char) (c ^ 0x80);
   2749     }
   2750   }
   2751   return o;
   2752 }
   2753 
   2754 
   2755 /**
   2756  * Collect the text of a MOBI book and emit it.  Handles uncompressed
   2757  * and PalmDOC-compressed books; HUFF/CDIC compression and encrypted
   2758  * books are skipped.
   2759  *
   2760  * @param ec extraction context
   2761  * @param rec record 0
   2762  * @param rec_len number of bytes in @a rec (at least 16)
   2763  * @param hdr_len length of the MOBI header, 0 if there is none
   2764  * @param num_records number of records in the database
   2765  * @param size size of the file
   2766  */
   2767 static void
   2768 mobi_fulltext (struct EXTRACTOR_ExtractContext *ec,
   2769                const unsigned char *rec,
   2770                size_t rec_len,
   2771                uint32_t hdr_len,
   2772                uint16_t num_records,
   2773                uint64_t size)
   2774 {
   2775   struct EXTRACTOR_FullText ft;
   2776   uint16_t compression = EXTRACTOR_forensic_be16_ (&rec[0]);
   2777   uint16_t text_records = EXTRACTOR_forensic_be16_ (&rec[8]);
   2778   uint16_t extra_flags = 0;
   2779   const char *cs = "windows-1252";
   2780   unsigned char *info = NULL;
   2781   unsigned char *in = NULL;
   2782   unsigned char *text = NULL;
   2783   size_t text_len = 0;
   2784   size_t text_cap = 0;
   2785   unsigned int n_info;
   2786   char *utf8;
   2787   size_t utf8_len;
   2788 
   2789   if (0 != EXTRACTOR_forensic_be16_ (&rec[12]))
   2790     return;   /* encrypted */
   2791   if ( (1 != compression) &&
   2792        (2 != compression) )
   2793     return;   /* HUFF/CDIC (17480) or unknown */
   2794   if (hdr_len >= 28)
   2795   {
   2796     if (65001 == EXTRACTOR_forensic_be32_ (&rec[16 + 12]))
   2797       cs = "utf-8";
   2798   }
   2799   /* the extra data flags exist from MOBI header length 0xE4 on */
   2800   if ( (hdr_len >= 0xE4) &&
   2801        (rec_len >= 16 + 0xE4) )
   2802     extra_flags = EXTRACTOR_forensic_be16_ (&rec[16 + 0xE2]);
   2803   if (text_records >= num_records)
   2804     text_records = num_records - 1;
   2805   if (0 == text_records)
   2806     return;
   2807   /* offsets of records 0 .. text_records + 1 */
   2808   n_info = (unsigned int) text_records + 2;
   2809   if (n_info > num_records)
   2810     n_info = num_records;
   2811   if (NULL == (info = malloc (8 * (size_t) n_info)))
   2812     return;
   2813   if (! EXTRACTOR_forensic_read_ (ec, 78, info, 8 * (size_t) n_info))
   2814     goto CLEANUP;
   2815   if ( (NULL == (in = malloc (MAX_TEXT_RECORD))) )
   2816     goto CLEANUP;
   2817   for (unsigned int r = 1; r <= text_records; r++)
   2818   {
   2819     uint64_t start = EXTRACTOR_forensic_be32_ (&info[8 * r]);
   2820     uint64_t stop = (r + 1 < n_info)
   2821                     ? EXTRACTOR_forensic_be32_ (&info[8 * (r + 1)])
   2822                     : size;
   2823     size_t len;
   2824     size_t strip;
   2825 
   2826     if ( (stop <= start) ||
   2827          (stop > size) ||
   2828          (stop - start > MAX_TEXT_RECORD) )
   2829       continue;
   2830     len = (size_t) (stop - start);
   2831     if (! EXTRACTOR_forensic_read_ (ec, (int64_t) start, in, len))
   2832       continue;
   2833     strip = trailing_size (in, len, extra_flags);
   2834     len -= strip;
   2835     if (text_len + MAX_TEXT_RECORD_OUT > text_cap)
   2836     {
   2837       size_t ncap = (0 == text_cap) ? 256 * 1024 : 2 * text_cap;
   2838       unsigned char *nt;
   2839 
   2840       if (ncap > MAX_BOOK_MARKUP)
   2841         break;
   2842       if (NULL == (nt = realloc (text, ncap)))
   2843         break;
   2844       text = nt;
   2845       text_cap = ncap;
   2846     }
   2847     if (1 == compression)
   2848     {
   2849       if (len > MAX_TEXT_RECORD_OUT)
   2850         len = MAX_TEXT_RECORD_OUT;
   2851       memcpy (&text[text_len], in, len);
   2852       text_len += len;
   2853     }
   2854     else
   2855     {
   2856       text_len += palmdoc_decompress (in,
   2857                                       len,
   2858                                       &text[text_len],
   2859                                       MAX_TEXT_RECORD_OUT);
   2860     }
   2861   }
   2862   if (0 == text_len)
   2863     goto CLEANUP;
   2864   utf8 = convert_to_utf8 ((const char *) text, text_len, cs, &utf8_len);
   2865   if (NULL == utf8)
   2866     goto CLEANUP;
   2867   free (text);
   2868   text = NULL;
   2869   EXTRACTOR_fulltext_init_ (&ft);
   2870   EXTRACTOR_fulltext_set_context_ (&ft, ec);
   2871   markup_to_text (&ft, utf8, utf8_len);
   2872   free (utf8);
   2873   (void) EXTRACTOR_fulltext_emit_ (&ft, ec, PLUGIN_NAME);
   2874   EXTRACTOR_fulltext_clear_ (&ft);
   2875 CLEANUP:
   2876   free (text);
   2877   free (in);
   2878   free (info);
   2879 }
   2880 
   2881 
   2882 /**
   2883  * Convert a Palm database time stamp.
   2884  *
   2885  * The format says seconds since 1904, but writers disagree and some use
   2886  * the Unix epoch.  The two ranges do not overlap for any date between
   2887  * 1970 and 2036, so the value itself says which convention was used.
   2888  *
   2889  * @param raw the 32 bit field
   2890  * @return seconds since 1970, 0 if the field was never filled in
   2891  */
   2892 static int64_t
   2893 palm_time (uint32_t raw)
   2894 {
   2895   if (0 == raw)
   2896     return 0;
   2897   if (((int64_t) raw) >= PALM_EPOCH_OFFSET)
   2898     return ((int64_t) raw) - PALM_EPOCH_OFFSET;
   2899   return (int64_t) raw;
   2900 }
   2901 
   2902 
   2903 /**
   2904  * Report one EXTH record.
   2905  *
   2906  * @param ec extraction context
   2907  * @param type EXTH record type
   2908  * @param data the record's payload
   2909  * @param len number of bytes in @a data
   2910  * @param[in,out] have_title set to 1 once a title has been reported
   2911  * @return 1 if the caller should stop extracting, 0 to continue
   2912  */
   2913 static int
   2914 emit_exth (struct EXTRACTOR_ExtractContext *ec,
   2915            uint32_t type,
   2916            const unsigned char *data,
   2917            size_t len,
   2918            int *have_title)
   2919 {
   2920   enum EXTRACTOR_MetaType mt;
   2921 
   2922   switch (type)
   2923   {
   2924   case 100: mt = EXTRACTOR_METATYPE_AUTHOR_NAME; break;
   2925   case 101: mt = EXTRACTOR_METATYPE_PUBLISHER; break;
   2926   case 103: mt = EXTRACTOR_METATYPE_DESCRIPTION; break;
   2927   case 104: mt = EXTRACTOR_METATYPE_ISBN; break;
   2928   case 105: mt = EXTRACTOR_METATYPE_KEYWORDS; break;
   2929   case 106: mt = EXTRACTOR_METATYPE_PUBLICATION_DATE; break;
   2930   case 108:
   2931     /* the tool that produced the file stamps itself here; calibre and
   2932        kindlegen both do */
   2933     mt = EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE;
   2934     break;
   2935   case 109: mt = EXTRACTOR_METATYPE_RIGHTS; break;
   2936   case 112: mt = EXTRACTOR_METATYPE_URI; break;
   2937   case 113: mt = EXTRACTOR_METATYPE_SERIAL; break;     /* ASIN */
   2938   case 208:
   2939     /* per-copy marking on files that came from a store */
   2940     mt = EXTRACTOR_METATYPE_WATERMARK;
   2941     break;
   2942   case 502: mt = EXTRACTOR_METATYPE_MODIFICATION_DATE; break;
   2943   case 503:
   2944     mt = EXTRACTOR_METATYPE_TITLE;
   2945     *have_title = 1;
   2946     break;
   2947   case 524: mt = EXTRACTOR_METATYPE_LANGUAGE; break;
   2948   case 501:
   2949     /* `EBOK' for a store book, `PDOC' for a personal document that was
   2950        side-loaded or mailed to a device */
   2951     return EXTRACTOR_forensic_emit_ (ec,
   2952                                      PLUGIN_NAME,
   2953                                      EXTRACTOR_METATYPE_ATTRIBUTES,
   2954                                      "cdetype: %.*s",
   2955                                      (int) ((len > 16) ? 16 : len),
   2956                                      (const char *) data);
   2957   case 209:
   2958     /* "tamper proof keys": binary, and specific to the copy rather than
   2959        to the book, so worth reporting even though we cannot decode it */
   2960     return EXTRACTOR_forensic_emit_hex_ (ec,
   2961                                          PLUGIN_NAME,
   2962                                          EXTRACTOR_METATYPE_WATERMARK,
   2963                                          data,
   2964                                          (len > 32) ? 32 : len);
   2965   default:
   2966     return 0;
   2967   }
   2968   return EXTRACTOR_forensic_emit_text_ (ec,
   2969                                         PLUGIN_NAME,
   2970                                         mt,
   2971                                         (const char *) data,
   2972                                         len);
   2973 }
   2974 
   2975 
   2976 /**
   2977  * Walk the EXTH header of a MOBI file.
   2978  *
   2979  * @param ec extraction context
   2980  * @param rec record 0
   2981  * @param rec_len number of bytes in @a rec
   2982  * @param off offset of the EXTH header within @a rec
   2983  * @param[in,out] have_title set to 1 once a title has been reported
   2984  * @return 1 if the caller should stop extracting, 0 to continue
   2985  */
   2986 static int
   2987 parse_exth (struct EXTRACTOR_ExtractContext *ec,
   2988             const unsigned char *rec,
   2989             size_t rec_len,
   2990             size_t off,
   2991             int *have_title)
   2992 {
   2993   uint32_t count;
   2994   uint32_t exth_len;
   2995   size_t end;
   2996   size_t p;
   2997 
   2998   if ( (off > rec_len) ||
   2999        (rec_len - off < 12) )
   3000     return 0;
   3001   if (0 != memcmp (&rec[off],
   3002                    "EXTH",
   3003                    4))
   3004     return 0;
   3005   exth_len = EXTRACTOR_forensic_be32_ (&rec[off + 4]);
   3006   count = EXTRACTOR_forensic_be32_ (&rec[off + 8]);
   3007   end = rec_len;
   3008   if ( (exth_len >= 12) &&
   3009        (exth_len <= rec_len - off) )
   3010     end = off + exth_len;
   3011   if (count > MAX_EXTH)
   3012     count = MAX_EXTH;
   3013   p = off + 12;
   3014   for (uint32_t i = 0; i < count; i++)
   3015   {
   3016     uint32_t type;
   3017     uint32_t rlen;
   3018 
   3019     if ( (p + 8 > end) ||
   3020          (p + 8 < p) )
   3021       break;
   3022     type = EXTRACTOR_forensic_be32_ (&rec[p]);
   3023     rlen = EXTRACTOR_forensic_be32_ (&rec[p + 4]);
   3024     if (rlen < 8)
   3025       break;   /* no forward progress; a malformed file must not spin */
   3026     if (((size_t) rlen) > end - p)
   3027       break;
   3028     if (0 !=
   3029         emit_exth (ec,
   3030                    type,
   3031                    &rec[p + 8],
   3032                    (size_t) rlen - 8,
   3033                    have_title))
   3034       return 1;
   3035     p += rlen;
   3036   }
   3037   return 0;
   3038 }
   3039 
   3040 
   3041 /**
   3042  * Handle a Palm database that identifies itself as a MOBI book.
   3043  *
   3044  * @param ec extraction context
   3045  * @param hdr the 78 byte PDB header
   3046  */
   3047 static void
   3048 extract_mobi (struct EXTRACTOR_ExtractContext *ec,
   3049               const unsigned char *hdr)
   3050 {
   3051   unsigned char info[16];
   3052   unsigned char *rec = NULL;
   3053   uint64_t size;
   3054   uint32_t rec0_off;
   3055   uint32_t rec1_off;
   3056   uint32_t hdr_len = 0;
   3057   uint16_t num_records;
   3058   size_t rec_len;
   3059   int have_title = 0;
   3060   int rec_ok = 0;
   3061 
   3062   if (0 !=
   3063       ec->proc (ec->cls,
   3064                 PLUGIN_NAME,
   3065                 EXTRACTOR_METATYPE_MIMETYPE,
   3066                 EXTRACTOR_METAFORMAT_UTF8,
   3067                 "text/plain",
   3068                 "application/x-mobipocket-ebook",
   3069                 strlen ("application/x-mobipocket-ebook") + 1))
   3070     return;
   3071   if (0 !=
   3072       EXTRACTOR_forensic_emit_unix_time_ (ec,
   3073                                           PLUGIN_NAME,
   3074                                           EXTRACTOR_METATYPE_CREATION_DATE,
   3075                                           palm_time (
   3076                                             EXTRACTOR_forensic_be32_ (
   3077                                               &hdr[36]))))
   3078     return;
   3079   if (0 !=
   3080       EXTRACTOR_forensic_emit_unix_time_ (
   3081         ec,
   3082         PLUGIN_NAME,
   3083         EXTRACTOR_METATYPE_MODIFICATION_DATE,
   3084         palm_time (EXTRACTOR_forensic_be32_ (&hdr[40]))))
   3085     return;
   3086   num_records = EXTRACTOR_forensic_be16_ (&hdr[76]);
   3087   if (0 !=
   3088       EXTRACTOR_forensic_emit_ (ec,
   3089                                 PLUGIN_NAME,
   3090                                 EXTRACTOR_METATYPE_ENTRY_COUNT,
   3091                                 "%u",
   3092                                 (unsigned int) num_records))
   3093     return;
   3094   size = ec->get_size (ec->cls);
   3095   if ( (2 > num_records) ||
   3096        (UINT64_MAX == size) )
   3097     goto NAME_ONLY;
   3098   /* the record info list follows the header; entry i gives the offset
   3099      of record i, so record 0 runs up to the start of record 1 */
   3100   if (! EXTRACTOR_forensic_read_ (ec,
   3101                                   78,
   3102                                   info,
   3103                                   16))
   3104     goto NAME_ONLY;
   3105   rec0_off = EXTRACTOR_forensic_be32_ (&info[0]);
   3106   rec1_off = EXTRACTOR_forensic_be32_ (&info[8]);
   3107   if ( (rec1_off <= rec0_off) ||
   3108        (((uint64_t) rec1_off) > size) )
   3109     goto NAME_ONLY;
   3110   rec_len = (size_t) (rec1_off - rec0_off);
   3111   if (rec_len > MAX_RECORD0)
   3112     rec_len = MAX_RECORD0;
   3113   if (rec_len < 16)
   3114     goto NAME_ONLY;
   3115   if (NULL == (rec = malloc (rec_len)))
   3116     goto NAME_ONLY;
   3117   if (! EXTRACTOR_forensic_read_ (ec,
   3118                                   (int64_t) rec0_off,
   3119                                   rec,
   3120                                   rec_len))
   3121     goto NAME_ONLY;
   3122   rec_ok = 1;
   3123   /* PalmDOC header: encryption type at offset 12 */
   3124   switch (EXTRACTOR_forensic_be16_ (&rec[12]))
   3125   {
   3126   case 0:
   3127     break;   /* no DRM; saying so would be noise */
   3128   case 1:
   3129     if (0 !=
   3130         EXTRACTOR_forensic_emit_ (ec,
   3131                                   PLUGIN_NAME,
   3132                                   EXTRACTOR_METATYPE_ENCRYPTION,
   3133                                   "old Mobipocket encryption"))
   3134       goto CLEANUP;
   3135     break;
   3136   case 2:
   3137     if (0 !=
   3138         EXTRACTOR_forensic_emit_ (ec,
   3139                                   PLUGIN_NAME,
   3140                                   EXTRACTOR_METATYPE_ENCRYPTION,
   3141                                   "Mobipocket DRM"))
   3142       goto CLEANUP;
   3143     break;
   3144   default:
   3145     if (0 !=
   3146         EXTRACTOR_forensic_emit_ (ec,
   3147                                   PLUGIN_NAME,
   3148                                   EXTRACTOR_METATYPE_ENCRYPTION,
   3149                                   "unknown encryption type %u",
   3150                                   (unsigned int)
   3151                                   EXTRACTOR_forensic_be16_ (&rec[12])))
   3152       goto CLEANUP;
   3153     break;
   3154   }
   3155   if ( (rec_len < 32) ||
   3156        (0 != memcmp (&rec[16],
   3157                      "MOBI",
   3158                      4)) )
   3159     goto NAME_ONLY;   /* PalmDOC without a MOBI header */
   3160   hdr_len = EXTRACTOR_forensic_be32_ (&rec[20]);
   3161   if ( (hdr_len < 24) ||
   3162        (((uint64_t) hdr_len) + 16 > rec_len) )
   3163   {
   3164     hdr_len = 0;
   3165     goto NAME_ONLY;
   3166   }
   3167   if (hdr_len >= 28)
   3168   {
   3169     const char *cs = NULL;
   3170 
   3171     switch (EXTRACTOR_forensic_be32_ (&rec[16 + 12]))
   3172     {
   3173     case 1252: cs = "windows-1252"; break;
   3174     case 65001: cs = "UTF-8"; break;
   3175     default: break;
   3176     }
   3177     if ( (NULL != cs) &&
   3178          (0 !=
   3179           EXTRACTOR_forensic_emit_ (ec,
   3180                                     PLUGIN_NAME,
   3181                                     EXTRACTOR_METATYPE_CHARACTER_SET,
   3182                                     "%s",
   3183                                     cs)) )
   3184       goto CLEANUP;
   3185   }
   3186   /* EXTH flags live at MOBI header offset 112; bit 0x40 says that an
   3187      EXTH header follows the MOBI header */
   3188   if ( (hdr_len >= 116) &&
   3189        (0 != (EXTRACTOR_forensic_be32_ (&rec[16 + 112]) & 0x40)) )
   3190   {
   3191     if (0 !=
   3192         parse_exth (ec,
   3193                     rec,
   3194                     rec_len,
   3195                     (size_t) hdr_len + 16,
   3196                     &have_title))
   3197       goto CLEANUP;
   3198   }
   3199   if ( (! have_title) &&
   3200        (hdr_len >= 76) )
   3201   {
   3202     uint32_t fn_off = EXTRACTOR_forensic_be32_ (&rec[16 + 68]);
   3203     uint32_t fn_len = EXTRACTOR_forensic_be32_ (&rec[16 + 72]);
   3204 
   3205     if ( (fn_len > 0) &&
   3206          (fn_len <= EXTRACTOR_FORENSIC_MAX_STRING) &&
   3207          (((uint64_t) fn_off) + fn_len <= rec_len) )
   3208     {
   3209       if (0 !=
   3210           EXTRACTOR_forensic_emit_text_ (ec,
   3211                                          PLUGIN_NAME,
   3212                                          EXTRACTOR_METATYPE_TITLE,
   3213                                          (const char *) &rec[fn_off],
   3214                                          fn_len))
   3215         goto CLEANUP;
   3216       have_title = 1;
   3217     }
   3218   }
   3219 NAME_ONLY:
   3220   if ( (! have_title) &&
   3221        (0 != EXTRACTOR_forensic_emit_text_ (ec,
   3222                                             PLUGIN_NAME,
   3223                                             EXTRACTOR_METATYPE_TITLE,
   3224                                             (const char *) hdr,
   3225                                             32)) )
   3226     goto CLEANUP;
   3227   if (rec_ok &&
   3228       (EXTRACTOR_fulltext_requested_ (ec->config)) )
   3229     mobi_fulltext (ec,
   3230                    rec,
   3231                    rec_len,
   3232                    hdr_len,
   3233                    num_records,
   3234                    size);
   3235 CLEANUP:
   3236   free (rec);
   3237 }
   3238 
   3239 
   3240 /**
   3241  * Main entry method for the ebook extraction plugin.
   3242  *
   3243  * @param ec extraction context provided to the plugin
   3244  */
   3245 void
   3246 EXTRACTOR_ebook_extract_method (struct EXTRACTOR_ExtractContext *ec);
   3247 
   3248 void
   3249 EXTRACTOR_ebook_extract_method (struct EXTRACTOR_ExtractContext *ec)
   3250 {
   3251   unsigned char hdr[78];
   3252   struct EXTRACTOR_UnzipFile *uf;
   3253 
   3254   if (! EXTRACTOR_forensic_read_ (ec,
   3255                                   0,
   3256                                   hdr,
   3257                                   4))
   3258     return;
   3259   if (0 != memcmp (hdr,
   3260                    "PK\x03\x04",
   3261                    4))
   3262   {
   3263     /* not a ZIP; the only other thing we handle is a Palm database,
   3264        whose type/creator pair sits at offset 60 */
   3265     if (! EXTRACTOR_forensic_read_ (ec,
   3266                                     0,
   3267                                     hdr,
   3268                                     sizeof (hdr)))
   3269       return;
   3270     if (0 == memcmp (&hdr[60],
   3271                      "BOOKMOBI",
   3272                      8))
   3273       extract_mobi (ec,
   3274                     hdr);
   3275     return;
   3276   }
   3277   if (NULL == (uf = EXTRACTOR_common_unzip_open (ec)))
   3278     return;
   3279   /* a plain ZIP, a docx and an ODF file all start `PK\003\004', so
   3280      claim the file only if it carries the OCF structure */
   3281   if (has_member (uf,
   3282                   "META-INF/container.xml"))
   3283   {
   3284     extract_epub (ec,
   3285                   uf);
   3286   }
   3287   else
   3288   {
   3289     size_t len;
   3290     char *mt = read_member (uf,
   3291                             "mimetype",
   3292                             64,
   3293                             &len);
   3294 
   3295     if ( (NULL != mt) &&
   3296          (len >= strlen ("application/epub+zip")) &&
   3297          (0 == memcmp (mt,
   3298                        "application/epub+zip",
   3299                        strlen ("application/epub+zip"))) )
   3300     {
   3301       free (mt);
   3302       extract_epub (ec,
   3303                     uf);
   3304     }
   3305     else
   3306     {
   3307       free (mt);
   3308     }
   3309   }
   3310   (void) EXTRACTOR_common_unzip_close (uf);
   3311 }
   3312 
   3313 
   3314 /* end of ebook_extractor.c */