libextractor

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

ebook_extractor.c (39357B)


      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  * References:
     33  * - EPUB 3 OCF and Package Document, https://www.w3.org/TR/epub-33/
     34  * - MOBI format, https://wiki.mobileread.com/wiki/MOBI
     35  * - PalmDOC / PDB, https://wiki.mobileread.com/wiki/PDB
     36  */
     37 #include "platform.h"
     38 #include "extractor.h"
     39 #include "forensics.h"
     40 #include "unzip.h"
     41 
     42 
     43 /**
     44  * Name we report our meta data under.
     45  */
     46 #define PLUGIN_NAME "ebook"
     47 
     48 /**
     49  * Never read more than this from `META-INF/container.xml'.  It holds a
     50  * handful of paths; anything larger is padding.
     51  */
     52 #define MAX_CONTAINER (64 * 1024)
     53 
     54 /**
     55  * Never read more than this from the OPF package document.  Real ones
     56  * are a few kilobytes plus the manifest.
     57  */
     58 #define MAX_OPF (256 * 1024)
     59 
     60 /**
     61  * Never read more than this from record 0 of a Palm database.  The MOBI
     62  * and EXTH headers together are a few kilobytes.
     63  */
     64 #define MAX_RECORD0 (64 * 1024)
     65 
     66 /**
     67  * Upper bound on the number of EXTH records we will walk.
     68  */
     69 #define MAX_EXTH 1024
     70 
     71 /**
     72  * Upper bound on the number of XML elements we will walk in the OPF.
     73  */
     74 #define MAX_ELEMENTS 8192
     75 
     76 /**
     77  * Seconds between the Palm epoch (1904-01-01) and the Unix epoch.
     78  */
     79 #define PALM_EPOCH_OFFSET 2082844800LL
     80 
     81 
     82 /**
     83  * Find @a needle in @a hay.  `memmem()' is a GNU extension, and this is
     84  * only ever run over buffers of a few hundred kilobytes.
     85  *
     86  * @param hay buffer to search
     87  * @param hlen number of bytes in @a hay
     88  * @param needle bytes to look for
     89  * @param nlen number of bytes in @a needle
     90  * @return pointer into @a hay, NULL if @a needle does not occur
     91  */
     92 static const char *
     93 mem_find (const char *hay,
     94           size_t hlen,
     95           const char *needle,
     96           size_t nlen)
     97 {
     98   if ( (0 == nlen) ||
     99        (nlen > hlen) )
    100     return NULL;
    101   for (size_t i = 0; i + nlen <= hlen; i++)
    102     if (0 == memcmp (&hay[i],
    103                      needle,
    104                      nlen))
    105       return &hay[i];
    106   return NULL;
    107 }
    108 
    109 
    110 /**
    111  * Is @a c a character that may appear in an XML name?
    112  *
    113  * @param c character to test
    114  * @return 1 if @a c may be part of a name
    115  */
    116 static int
    117 is_name_char (char c)
    118 {
    119   return ( ( ('a' <= c) && ('z' >= c) ) ||
    120            ( ('A' <= c) && ('Z' >= c) ) ||
    121            ( ('0' <= c) && ('9' >= c) ) ||
    122            ('_' == c) || ('-' == c) || ('.' == c) || (':' == c) );
    123 }
    124 
    125 
    126 /**
    127  * Is @a c XML white space?
    128  *
    129  * @param c character to test
    130  * @return 1 if @a c is white space
    131  */
    132 static int
    133 is_space (char c)
    134 {
    135   return (' ' == c) || ('\t' == c) || ('\r' == c) || ('\n' == c);
    136 }
    137 
    138 
    139 /**
    140  * Append @a cp to @a out as UTF-8.
    141  *
    142  * @param out output buffer
    143  * @param outsz number of bytes available in @a out
    144  * @param pos current write position
    145  * @param cp code point to append
    146  * @return new write position, unchanged if @a cp did not fit
    147  */
    148 static size_t
    149 utf8_put (char *out,
    150           size_t outsz,
    151           size_t pos,
    152           uint32_t cp)
    153 {
    154   if ( (cp > 0x10FFFF) ||
    155        ( (0xD800 <= cp) && (0xDFFF >= cp) ) )
    156     return pos;
    157   if (cp < 0x80)
    158   {
    159     if (pos + 1 > outsz)
    160       return pos;
    161     out[pos++] = (char) cp;
    162   }
    163   else if (cp < 0x800)
    164   {
    165     if (pos + 2 > outsz)
    166       return pos;
    167     out[pos++] = (char) (0xC0 | (cp >> 6));
    168     out[pos++] = (char) (0x80 | (cp & 0x3F));
    169   }
    170   else if (cp < 0x10000)
    171   {
    172     if (pos + 3 > outsz)
    173       return pos;
    174     out[pos++] = (char) (0xE0 | (cp >> 12));
    175     out[pos++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    176     out[pos++] = (char) (0x80 | (cp & 0x3F));
    177   }
    178   else
    179   {
    180     if (pos + 4 > outsz)
    181       return pos;
    182     out[pos++] = (char) (0xF0 | (cp >> 18));
    183     out[pos++] = (char) (0x80 | ((cp >> 12) & 0x3F));
    184     out[pos++] = (char) (0x80 | ((cp >> 6) & 0x3F));
    185     out[pos++] = (char) (0x80 | (cp & 0x3F));
    186   }
    187   return pos;
    188 }
    189 
    190 
    191 /**
    192  * Copy XML character data, resolving the five predefined entities and
    193  * numeric character references and collapsing runs of white space.
    194  *
    195  * The OPF fields we read are laid out across indented lines, so without
    196  * the white space collapsing every value would come back with the
    197  * source file's line breaks embedded in it.
    198  *
    199  * @param data the character data
    200  * @param len number of bytes in @a data
    201  * @param[out] out where to write the result, NUL-terminated
    202  * @param outsz number of bytes available in @a out, at least 2
    203  * @return number of bytes written, not counting the NUL
    204  */
    205 static size_t
    206 xml_text (const char *data,
    207           size_t len,
    208           char *out,
    209           size_t outsz)
    210 {
    211   size_t o = 0;
    212   size_t i = 0;
    213   int pending_space = 0;
    214 
    215   while ( (i < len) &&
    216           (o + 1 < outsz) )
    217   {
    218     if (is_space (data[i]))
    219     {
    220       if (0 != o)
    221         pending_space = 1;
    222       i++;
    223       continue;
    224     }
    225     if (pending_space)
    226     {
    227       out[o++] = ' ';
    228       pending_space = 0;
    229       if (o + 1 >= outsz)
    230         break;
    231     }
    232     if ('&' != data[i])
    233     {
    234       out[o++] = data[i++];
    235       continue;
    236     }
    237     {
    238       const char *semi = memchr (&data[i],
    239                                  ';',
    240                                  len - i);
    241       size_t elen;
    242 
    243       if ( (NULL == semi) ||
    244            ( (size_t) (semi - &data[i]) > 12) )
    245       {
    246         out[o++] = data[i++];   /* a bare '&', which is not legal XML
    247                                    but does occur; pass it through */
    248         continue;
    249       }
    250       elen = (size_t) (semi - &data[i]) - 1;   /* between '&' and ';' */
    251       if ( (3 == elen) && (0 == memcmp (&data[i + 1], "amp", 3)) )
    252         out[o++] = '&';
    253       else if ( (2 == elen) && (0 == memcmp (&data[i + 1], "lt", 2)) )
    254         out[o++] = '<';
    255       else if ( (2 == elen) && (0 == memcmp (&data[i + 1], "gt", 2)) )
    256         out[o++] = '>';
    257       else if ( (4 == elen) && (0 == memcmp (&data[i + 1], "quot", 4)) )
    258         out[o++] = '"';
    259       else if ( (4 == elen) && (0 == memcmp (&data[i + 1], "apos", 4)) )
    260         out[o++] = '\'';
    261       else if ( (2 <= elen) && ('#' == data[i + 1]) )
    262       {
    263         uint32_t cp = 0;
    264         size_t k = i + 2;
    265         int base = 10;
    266 
    267         if ( ('x' == data[k]) || ('X' == data[k]) )
    268         {
    269           base = 16;
    270           k++;
    271         }
    272         for (; k < (size_t) (semi - data); k++)
    273         {
    274           unsigned int d;
    275 
    276           if ( ('0' <= data[k]) && ('9' >= data[k]) )
    277             d = (unsigned int) (data[k] - '0');
    278           else if ( (16 == base) && ('a' <= data[k]) && ('f' >= data[k]) )
    279             d = (unsigned int) (data[k] - 'a') + 10;
    280           else if ( (16 == base) && ('A' <= data[k]) && ('F' >= data[k]) )
    281             d = (unsigned int) (data[k] - 'A') + 10;
    282           else
    283           {
    284             cp = 0;
    285             break;
    286           }
    287           if (cp > 0x10FFFF)
    288           {
    289             cp = 0;
    290             break;
    291           }
    292           cp = cp * (uint32_t) base + d;
    293         }
    294         o = utf8_put (out,
    295                       outsz - 1,
    296                       o,
    297                       cp);
    298       }
    299       else
    300       {
    301         /* an entity we do not know; drop it rather than guess */
    302       }
    303       i += elen + 2;
    304     }
    305   }
    306   out[o] = '\0';
    307   return o;
    308 }
    309 
    310 
    311 /**
    312  * Read the value of attribute @a name out of the body of a start tag.
    313  * A namespace prefix on the attribute is ignored, so a request for
    314  * `role' also matches `opf:role'.
    315  *
    316  * @param data the start tag body, after the element name
    317  * @param len number of bytes in @a data
    318  * @param name local name of the attribute
    319  * @param[out] out where to write the value, NUL-terminated
    320  * @param outsz number of bytes available in @a out, at least 2
    321  * @return 1 if the attribute was found, 0 if not
    322  */
    323 static int
    324 xml_attr (const char *data,
    325           size_t len,
    326           const char *name,
    327           char *out,
    328           size_t outsz)
    329 {
    330   size_t nlen = strlen (name);
    331 
    332   out[0] = '\0';
    333   if (nlen >= len)
    334     return 0;
    335   for (size_t i = 0; i + nlen < len; i++)
    336   {
    337     size_t k;
    338     char quote;
    339     const char *vend;
    340 
    341     if (0 != memcmp (&data[i],
    342                      name,
    343                      nlen))
    344       continue;
    345     /* the name must start where an attribute may start: right after
    346        white space, or right after a namespace prefix that does */
    347     if (0 == i)
    348       continue;
    349     if (is_space (data[i - 1]))
    350     {
    351       /* fine */
    352     }
    353     else if (':' == data[i - 1])
    354     {
    355       size_t j = i - 1;
    356 
    357       while ( (0 < j) &&
    358               is_name_char (data[j - 1]) &&
    359               (':' != data[j - 1]) )
    360         j--;
    361       if ( (0 == j) ||
    362            (! is_space (data[j - 1])) )
    363         continue;
    364     }
    365     else
    366     {
    367       continue;
    368     }
    369     k = i + nlen;
    370     while ( (k < len) &&
    371             is_space (data[k]) )
    372       k++;
    373     if ( (k >= len) ||
    374          ('=' != data[k]) )
    375       continue;
    376     k++;
    377     while ( (k < len) &&
    378             is_space (data[k]) )
    379       k++;
    380     if ( (k >= len) ||
    381          ( ('"' != data[k]) && ('\'' != data[k]) ) )
    382       continue;
    383     quote = data[k];
    384     k++;
    385     vend = memchr (&data[k],
    386                    quote,
    387                    len - k);
    388     if (NULL == vend)
    389       return 0;
    390     (void) xml_text (&data[k],
    391                      (size_t) (vend - &data[k]),
    392                      out,
    393                      outsz);
    394     return 1;
    395   }
    396   return 0;
    397 }
    398 
    399 
    400 /**
    401  * Does @a s, after an optional `urn:isbn:' prefix, look like an ISBN?
    402  *
    403  * @param s NUL-terminated candidate
    404  * @return 1 if @a s is 10 or 13 digits with optional separators
    405  */
    406 static int
    407 looks_like_isbn (const char *s)
    408 {
    409   size_t digits = 0;
    410 
    411   if (0 == strncasecmp (s,
    412                         "urn:isbn:",
    413                         strlen ("urn:isbn:")))
    414     s += strlen ("urn:isbn:");
    415   else if (0 == strncasecmp (s,
    416                              "isbn:",
    417                              strlen ("isbn:")))
    418     s += strlen ("isbn:");
    419   for (size_t i = 0; '\0' != s[i]; i++)
    420   {
    421     if ( ('-' == s[i]) || (' ' == s[i]) )
    422       continue;
    423     if ( ('0' <= s[i]) && ('9' >= s[i]) )
    424     {
    425       digits++;
    426       continue;
    427     }
    428     if ( ( ('X' == s[i]) || ('x' == s[i]) ) &&
    429          (9 == digits) &&
    430          ('\0' == s[i + 1]) )
    431     {
    432       digits++;
    433       continue;
    434     }
    435     return 0;
    436   }
    437   return (10 == digits) || (13 == digits);
    438 }
    439 
    440 
    441 /**
    442  * Read a whole member out of a ZIP archive.
    443  *
    444  * @param uf the archive
    445  * @param name member to read
    446  * @param cap never read more than this many bytes
    447  * @param[out] len number of bytes read
    448  * @return the member's contents with a NUL appended, NULL on error;
    449  *         caller must free
    450  */
    451 static char *
    452 read_member (struct EXTRACTOR_UnzipFile *uf,
    453              const char *name,
    454              size_t cap,
    455              size_t *len)
    456 {
    457   struct EXTRACTOR_UnzipFileInfo fi;
    458   char *buf;
    459   size_t size;
    460   size_t got = 0;
    461 
    462   *len = 0;
    463   if (EXTRACTOR_UNZIP_OK !=
    464       EXTRACTOR_common_unzip_go_find_local_file (uf,
    465                                                  name,
    466                                                  2))
    467     return NULL;
    468   if (EXTRACTOR_UNZIP_OK !=
    469       EXTRACTOR_common_unzip_get_current_file_info (uf,
    470                                                     &fi,
    471                                                     NULL, 0,
    472                                                     NULL, 0,
    473                                                     NULL, 0))
    474     return NULL;
    475   size = (size_t) fi.uncompressed_size;
    476   if (0 == size)
    477     return NULL;
    478   if (size > cap)
    479     size = cap;
    480   if (NULL == (buf = malloc (size + 1)))
    481     return NULL;
    482   if (EXTRACTOR_UNZIP_OK !=
    483       EXTRACTOR_common_unzip_open_current_file (uf))
    484   {
    485     free (buf);
    486     return NULL;
    487   }
    488   while (got < size)
    489   {
    490     ssize_t ret;
    491 
    492     ret = EXTRACTOR_common_unzip_read_current_file (uf,
    493                                                     &buf[got],
    494                                                     size - got);
    495     if (0 >= ret)
    496       break;   /* error or end of member */
    497     if (((size_t) ret) > size - got)
    498       break;   /* cannot happen, but do not overrun if it does */
    499     got += (size_t) ret;
    500   }
    501   (void) EXTRACTOR_common_unzip_close_current_file (uf);
    502   if (0 == got)
    503   {
    504     free (buf);
    505     return NULL;
    506   }
    507   buf[got] = '\0';
    508   *len = got;
    509   return buf;
    510 }
    511 
    512 
    513 /**
    514  * Does the archive contain a member with this name?
    515  *
    516  * @param uf the archive
    517  * @param name member to look for
    518  * @return 1 if present
    519  */
    520 static int
    521 has_member (struct EXTRACTOR_UnzipFile *uf,
    522             const char *name)
    523 {
    524   return (EXTRACTOR_UNZIP_OK ==
    525           EXTRACTOR_common_unzip_go_find_local_file (uf,
    526                                                      name,
    527                                                      2));
    528 }
    529 
    530 
    531 /**
    532  * Walk the `<metadata>' element of an OPF package document and report
    533  * what it holds.
    534  *
    535  * This is a bounded text scan, not an XML parser: the OPF is untrusted
    536  * input and everything we want out of it is a flat list of leaf
    537  * elements, so pulling in a parser would buy nothing but attack surface.
    538  *
    539  * @param ec extraction context
    540  * @param opf the package document
    541  * @param len number of bytes in @a opf
    542  * @return 1 if the caller should stop extracting, 0 to continue
    543  */
    544 static int
    545 parse_opf_metadata (struct EXTRACTOR_ExtractContext *ec,
    546                     const char *opf,
    547                     size_t len)
    548 {
    549   char value[EXTRACTOR_FORENSIC_MAX_STRING];
    550   char attr[256];
    551   const char *mstart;
    552   const char *mend;
    553   const char *p;
    554   unsigned int elements = 0;
    555   /* the repeating elements are capped separately: a file with ten
    556      thousand <dc:subject> entries is characterised well enough by the
    557      first few dozen */
    558   unsigned int subjects = 0;
    559   unsigned int identifiers = 0;
    560   unsigned int people = 0;
    561 
    562   mstart = mem_find (opf,
    563                      len,
    564                      "<metadata",
    565                      strlen ("<metadata"));
    566   if (NULL == mstart)
    567     return 0;
    568   mend = mem_find (mstart,
    569                    len - (size_t) (mstart - opf),
    570                    "</metadata",
    571                    strlen ("</metadata"));
    572   if (NULL == mend)
    573     mend = opf + len;
    574   p = mstart;
    575   while ( (p < mend) &&
    576           (elements++ < MAX_ELEMENTS) )
    577   {
    578     const char *name;
    579     const char *local;
    580     const char *body;
    581     const char *text;
    582     size_t name_len;
    583     size_t body_len;
    584     size_t text_len;
    585     int self_closing;
    586     char quote = '\0';
    587     enum EXTRACTOR_MetaType type;
    588 
    589     p = memchr (p,
    590                 '<',
    591                 (size_t) (mend - p));
    592     if (NULL == p)
    593       break;
    594     p++;
    595     if (p >= mend)
    596       break;
    597     if ( ('/' == *p) || ('?' == *p) || ('!' == *p) )
    598       continue;
    599     name = p;
    600     while ( (p < mend) &&
    601             is_name_char (*p) )
    602       p++;
    603     name_len = (size_t) (p - name);
    604     if (0 == name_len)
    605       continue;
    606     body = p;
    607     while (p < mend)
    608     {
    609       if ('\0' != quote)
    610       {
    611         if (*p == quote)
    612           quote = '\0';
    613       }
    614       else if ( ('"' == *p) || ('\'' == *p) )
    615       {
    616         quote = *p;
    617       }
    618       else if ('>' == *p)
    619       {
    620         break;
    621       }
    622       p++;
    623     }
    624     if (p >= mend)
    625       break;
    626     body_len = (size_t) (p - body);
    627     self_closing = ( (0 < body_len) && ('/' == p[-1]) );
    628     if (self_closing)
    629       body_len--;
    630     p++;   /* step past '>' */
    631     text = p;
    632     text_len = 0;
    633     if (! self_closing)
    634     {
    635       const char *e = memchr (p,
    636                               '<',
    637                               (size_t) (mend - p));
    638 
    639       text_len = (NULL == e)
    640                  ? (size_t) (mend - p)
    641                  : (size_t) (e - p);
    642     }
    643     /* strip the namespace prefix; `dc:title' and `title' are the same
    644        element for our purposes because we are already inside
    645        <metadata> */
    646     local = name;
    647     for (size_t i = 0; i < name_len; i++)
    648       if (':' == name[i])
    649         local = &name[i + 1];
    650     name_len -= (size_t) (local - name);
    651 
    652 #define IS(s) ( (strlen (s) == name_len) && \
    653                 (0 == memcmp (local, s, name_len)) )
    654 
    655     if (IS ("meta"))
    656     {
    657       if ( (xml_attr (body,
    658                       body_len,
    659                       "name",
    660                       attr,
    661                       sizeof (attr))) &&
    662            (xml_attr (body,
    663                       body_len,
    664                       "content",
    665                       value,
    666                       sizeof (value))) )
    667       {
    668         if (0 == strcasecmp (attr,
    669                              "calibre:timestamp"))
    670           type = EXTRACTOR_METATYPE_CREATION_DATE;
    671         else if ( (0 == strcasecmp (attr,
    672                                     "generator")) ||
    673                   (0 == strcasecmp (attr,
    674                                     "calibre:generator")) )
    675           type = EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE;
    676         else
    677           continue;
    678         if (0 !=
    679             EXTRACTOR_forensic_emit_text_ (ec,
    680                                            PLUGIN_NAME,
    681                                            type,
    682                                            value,
    683                                            strlen (value)))
    684           return 1;
    685       }
    686       else if (xml_attr (body,
    687                          body_len,
    688                          "property",
    689                          attr,
    690                          sizeof (attr)))
    691       {
    692         if (0 != strcasecmp (attr,
    693                              "dcterms:modified"))
    694           continue;
    695         (void) xml_text (text,
    696                          text_len,
    697                          value,
    698                          sizeof (value));
    699         if (0 !=
    700             EXTRACTOR_forensic_emit_text_ (
    701               ec,
    702               PLUGIN_NAME,
    703               EXTRACTOR_METATYPE_MODIFICATION_DATE,
    704               value,
    705               strlen (value)))
    706           return 1;
    707       }
    708       continue;
    709     }
    710     if (0 == text_len)
    711       continue;
    712     (void) xml_text (text,
    713                      text_len,
    714                      value,
    715                      sizeof (value));
    716     if ('\0' == value[0])
    717       continue;
    718     if (IS ("title"))
    719       type = EXTRACTOR_METATYPE_TITLE;
    720     else if (IS ("creator"))
    721     {
    722       if (people++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    723         continue;
    724       type = EXTRACTOR_METATYPE_AUTHOR_NAME;
    725     }
    726     else if (IS ("publisher"))
    727       type = EXTRACTOR_METATYPE_PUBLISHER;
    728     else if (IS ("language"))
    729       type = EXTRACTOR_METATYPE_LANGUAGE;
    730     else if (IS ("date"))
    731       type = EXTRACTOR_METATYPE_PUBLICATION_DATE;
    732     else if (IS ("subject"))
    733     {
    734       if (subjects++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    735         continue;
    736       type = EXTRACTOR_METATYPE_KEYWORDS;
    737     }
    738     else if (IS ("description"))
    739       type = EXTRACTOR_METATYPE_DESCRIPTION;
    740     else if (IS ("rights"))
    741       type = EXTRACTOR_METATYPE_RIGHTS;
    742     else if (IS ("source"))
    743       type = EXTRACTOR_METATYPE_URI;
    744     else if (IS ("contributor"))
    745     {
    746       /* `bkp' is the MARC relator for the agent that made the file --
    747          which is the tool, and thus the provenance we are after */
    748       if ( (xml_attr (body,
    749                       body_len,
    750                       "role",
    751                       attr,
    752                       sizeof (attr))) &&
    753            (0 == strcasecmp (attr,
    754                              "bkp")) )
    755       {
    756         type = EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE;
    757       }
    758       else
    759       {
    760         if (people++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    761           continue;
    762         type = EXTRACTOR_METATYPE_CONTRIBUTOR_NAME;
    763       }
    764     }
    765     else if (IS ("identifier"))
    766     {
    767       int isbn = 0;
    768 
    769       if (identifiers++ >= EXTRACTOR_FORENSIC_MAX_ITEMS)
    770         continue;
    771 
    772       if (xml_attr (body,
    773                     body_len,
    774                     "scheme",
    775                     attr,
    776                     sizeof (attr)))
    777         isbn = (0 == strcasecmp (attr,
    778                                  "ISBN"));
    779       if (! isbn)
    780         isbn = looks_like_isbn (value);
    781       type = isbn
    782              ? EXTRACTOR_METATYPE_ISBN
    783              : EXTRACTOR_METATYPE_URI;
    784     }
    785     else
    786     {
    787       continue;
    788     }
    789     if (0 !=
    790         EXTRACTOR_forensic_emit_text_ (ec,
    791                                        PLUGIN_NAME,
    792                                        type,
    793                                        value,
    794                                        strlen (value)))
    795       return 1;
    796   }
    797 #undef IS
    798   return 0;
    799 }
    800 
    801 
    802 /**
    803  * Handle an EPUB: read `META-INF/container.xml' to find the OPF package
    804  * document, then report what the package document holds.
    805  *
    806  * @param ec extraction context
    807  * @param uf the opened archive
    808  */
    809 static void
    810 extract_epub (struct EXTRACTOR_ExtractContext *ec,
    811               struct EXTRACTOR_UnzipFile *uf)
    812 {
    813   char path[512];
    814   char attr[64];
    815   char *container;
    816   char *opf = NULL;
    817   size_t container_len;
    818   size_t opf_len = 0;
    819   const char *p;
    820   const char *q;
    821   unsigned int items = 0;
    822 
    823   if (0 !=
    824       ec->proc (ec->cls,
    825                 PLUGIN_NAME,
    826                 EXTRACTOR_METATYPE_MIMETYPE,
    827                 EXTRACTOR_METAFORMAT_UTF8,
    828                 "text/plain",
    829                 "application/epub+zip",
    830                 strlen ("application/epub+zip") + 1))
    831     return;
    832   container = read_member (uf,
    833                            "META-INF/container.xml",
    834                            MAX_CONTAINER,
    835                            &container_len);
    836   if (NULL != container)
    837   {
    838     p = mem_find (container,
    839                   container_len,
    840                   "full-path=",
    841                   strlen ("full-path="));
    842     if (NULL != p)
    843     {
    844       p += strlen ("full-path=");
    845       if ( ('"' == *p) || ('\'' == *p) )
    846       {
    847         char quote = *p;
    848 
    849         p++;
    850         q = memchr (p,
    851                     quote,
    852                     container_len - (size_t) (p - container));
    853         if (NULL != q)
    854           (void) xml_text (p,
    855                            (size_t) (q - p),
    856                            path,
    857                            sizeof (path));
    858         else
    859           path[0] = '\0';
    860       }
    861       else
    862       {
    863         path[0] = '\0';
    864       }
    865       if ('\0' != path[0])
    866         opf = read_member (uf,
    867                            path,
    868                            MAX_OPF,
    869                            &opf_len);
    870     }
    871     free (container);
    872   }
    873   if (NULL == opf)
    874   {
    875     /* no usable container.xml; the conventional locations are worth a
    876        try before giving up */
    877     static const char *guesses[] = {
    878       "OEBPS/content.opf",
    879       "content.opf",
    880       "OPS/content.opf",
    881       NULL
    882     };
    883 
    884     for (unsigned int i = 0; NULL != guesses[i]; i++)
    885     {
    886       opf = read_member (uf,
    887                          guesses[i],
    888                          MAX_OPF,
    889                          &opf_len);
    890       if (NULL != opf)
    891         break;
    892     }
    893   }
    894   if (NULL != opf)
    895   {
    896     /* the <package version="..."> attribute distinguishes EPUB 2 from
    897        EPUB 3, which changes what else we may expect to find */
    898     p = mem_find (opf,
    899                   opf_len,
    900                   "<package",
    901                   strlen ("<package"));
    902     if (NULL != p)
    903     {
    904       size_t tail = opf_len - (size_t) (p - opf);
    905       const char *gt;
    906 
    907       if (tail > 1024)
    908         tail = 1024;
    909       /* stop at the end of the start tag, so that a `version=' further
    910          down the document cannot be mistaken for this one */
    911       gt = memchr (p,
    912                    '>',
    913                    tail);
    914       if (NULL != gt)
    915         tail = (size_t) (gt - p);
    916       if (tail <= strlen ("<package"))
    917         tail = strlen ("<package");
    918       if (xml_attr (p + strlen ("<package"),
    919                     tail - strlen ("<package"),
    920                     "version",
    921                     attr,
    922                     sizeof (attr)))
    923       {
    924         if (0 !=
    925             EXTRACTOR_forensic_emit_text_ (ec,
    926                                            PLUGIN_NAME,
    927                                            EXTRACTOR_METATYPE_FORMAT_VERSION,
    928                                            attr,
    929                                            strlen (attr)))
    930           goto CLEANUP;
    931       }
    932     }
    933     if (0 !=
    934         parse_opf_metadata (ec,
    935                             opf,
    936                             opf_len))
    937       goto CLEANUP;
    938     /* count the manifest entries; `<itemref>' does not match because
    939        the character after `<item' has to be a name delimiter */
    940     for (size_t i = 0; i + strlen ("<item") < opf_len; i++)
    941       if ( (0 == memcmp (&opf[i],
    942                          "<item",
    943                          strlen ("<item"))) &&
    944            (! is_name_char (opf[i + strlen ("<item")])) )
    945         items++;
    946     if (0 != items)
    947     {
    948       if (0 !=
    949           EXTRACTOR_forensic_emit_ (ec,
    950                                     PLUGIN_NAME,
    951                                     EXTRACTOR_METATYPE_ENTRY_COUNT,
    952                                     "%u",
    953                                     items))
    954         goto CLEANUP;
    955     }
    956   }
    957   if (has_member (uf,
    958                   "META-INF/encryption.xml"))
    959   {
    960     if (0 !=
    961         EXTRACTOR_forensic_emit_ (
    962           ec,
    963           PLUGIN_NAME,
    964           EXTRACTOR_METATYPE_ENCRYPTION,
    965           "META-INF/encryption.xml present"
    966           " (DRM or font obfuscation)"))
    967       goto CLEANUP;
    968   }
    969   if (has_member (uf,
    970                   "META-INF/rights.xml"))
    971   {
    972     if (0 !=
    973         EXTRACTOR_forensic_emit_ (ec,
    974                                   PLUGIN_NAME,
    975                                   EXTRACTOR_METATYPE_ENCRYPTION,
    976                                   "META-INF/rights.xml present"
    977                                   " (Adobe Content Server DRM)"))
    978       goto CLEANUP;
    979   }
    980 CLEANUP:
    981   free (opf);
    982 }
    983 
    984 
    985 /**
    986  * Convert a Palm database time stamp.
    987  *
    988  * The format says seconds since 1904, but writers disagree and some use
    989  * the Unix epoch.  The two ranges do not overlap for any date between
    990  * 1970 and 2036, so the value itself says which convention was used.
    991  *
    992  * @param raw the 32 bit field
    993  * @return seconds since 1970, 0 if the field was never filled in
    994  */
    995 static int64_t
    996 palm_time (uint32_t raw)
    997 {
    998   if (0 == raw)
    999     return 0;
   1000   if (((int64_t) raw) >= PALM_EPOCH_OFFSET)
   1001     return ((int64_t) raw) - PALM_EPOCH_OFFSET;
   1002   return (int64_t) raw;
   1003 }
   1004 
   1005 
   1006 /**
   1007  * Report one EXTH record.
   1008  *
   1009  * @param ec extraction context
   1010  * @param type EXTH record type
   1011  * @param data the record's payload
   1012  * @param len number of bytes in @a data
   1013  * @param[in,out] have_title set to 1 once a title has been reported
   1014  * @return 1 if the caller should stop extracting, 0 to continue
   1015  */
   1016 static int
   1017 emit_exth (struct EXTRACTOR_ExtractContext *ec,
   1018            uint32_t type,
   1019            const unsigned char *data,
   1020            size_t len,
   1021            int *have_title)
   1022 {
   1023   enum EXTRACTOR_MetaType mt;
   1024 
   1025   switch (type)
   1026   {
   1027   case 100: mt = EXTRACTOR_METATYPE_AUTHOR_NAME; break;
   1028   case 101: mt = EXTRACTOR_METATYPE_PUBLISHER; break;
   1029   case 103: mt = EXTRACTOR_METATYPE_DESCRIPTION; break;
   1030   case 104: mt = EXTRACTOR_METATYPE_ISBN; break;
   1031   case 105: mt = EXTRACTOR_METATYPE_KEYWORDS; break;
   1032   case 106: mt = EXTRACTOR_METATYPE_PUBLICATION_DATE; break;
   1033   case 108:
   1034     /* the tool that produced the file stamps itself here; calibre and
   1035        kindlegen both do */
   1036     mt = EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE;
   1037     break;
   1038   case 109: mt = EXTRACTOR_METATYPE_RIGHTS; break;
   1039   case 112: mt = EXTRACTOR_METATYPE_URI; break;
   1040   case 113: mt = EXTRACTOR_METATYPE_SERIAL; break;     /* ASIN */
   1041   case 208:
   1042     /* per-copy marking on files that came from a store */
   1043     mt = EXTRACTOR_METATYPE_WATERMARK;
   1044     break;
   1045   case 502: mt = EXTRACTOR_METATYPE_MODIFICATION_DATE; break;
   1046   case 503:
   1047     mt = EXTRACTOR_METATYPE_TITLE;
   1048     *have_title = 1;
   1049     break;
   1050   case 524: mt = EXTRACTOR_METATYPE_LANGUAGE; break;
   1051   case 501:
   1052     /* `EBOK' for a store book, `PDOC' for a personal document that was
   1053        side-loaded or mailed to a device */
   1054     return EXTRACTOR_forensic_emit_ (ec,
   1055                                      PLUGIN_NAME,
   1056                                      EXTRACTOR_METATYPE_ATTRIBUTES,
   1057                                      "cdetype: %.*s",
   1058                                      (int) ((len > 16) ? 16 : len),
   1059                                      (const char *) data);
   1060   case 209:
   1061     /* "tamper proof keys": binary, and specific to the copy rather than
   1062        to the book, so worth reporting even though we cannot decode it */
   1063     return EXTRACTOR_forensic_emit_hex_ (ec,
   1064                                          PLUGIN_NAME,
   1065                                          EXTRACTOR_METATYPE_WATERMARK,
   1066                                          data,
   1067                                          (len > 32) ? 32 : len);
   1068   default:
   1069     return 0;
   1070   }
   1071   return EXTRACTOR_forensic_emit_text_ (ec,
   1072                                         PLUGIN_NAME,
   1073                                         mt,
   1074                                         (const char *) data,
   1075                                         len);
   1076 }
   1077 
   1078 
   1079 /**
   1080  * Walk the EXTH header of a MOBI file.
   1081  *
   1082  * @param ec extraction context
   1083  * @param rec record 0
   1084  * @param rec_len number of bytes in @a rec
   1085  * @param off offset of the EXTH header within @a rec
   1086  * @param[in,out] have_title set to 1 once a title has been reported
   1087  * @return 1 if the caller should stop extracting, 0 to continue
   1088  */
   1089 static int
   1090 parse_exth (struct EXTRACTOR_ExtractContext *ec,
   1091             const unsigned char *rec,
   1092             size_t rec_len,
   1093             size_t off,
   1094             int *have_title)
   1095 {
   1096   uint32_t count;
   1097   uint32_t exth_len;
   1098   size_t end;
   1099   size_t p;
   1100 
   1101   if ( (off > rec_len) ||
   1102        (rec_len - off < 12) )
   1103     return 0;
   1104   if (0 != memcmp (&rec[off],
   1105                    "EXTH",
   1106                    4))
   1107     return 0;
   1108   exth_len = EXTRACTOR_forensic_be32_ (&rec[off + 4]);
   1109   count = EXTRACTOR_forensic_be32_ (&rec[off + 8]);
   1110   end = rec_len;
   1111   if ( (exth_len >= 12) &&
   1112        (exth_len <= rec_len - off) )
   1113     end = off + exth_len;
   1114   if (count > MAX_EXTH)
   1115     count = MAX_EXTH;
   1116   p = off + 12;
   1117   for (uint32_t i = 0; i < count; i++)
   1118   {
   1119     uint32_t type;
   1120     uint32_t rlen;
   1121 
   1122     if ( (p + 8 > end) ||
   1123          (p + 8 < p) )
   1124       break;
   1125     type = EXTRACTOR_forensic_be32_ (&rec[p]);
   1126     rlen = EXTRACTOR_forensic_be32_ (&rec[p + 4]);
   1127     if (rlen < 8)
   1128       break;   /* no forward progress; a malformed file must not spin */
   1129     if (((size_t) rlen) > end - p)
   1130       break;
   1131     if (0 !=
   1132         emit_exth (ec,
   1133                    type,
   1134                    &rec[p + 8],
   1135                    (size_t) rlen - 8,
   1136                    have_title))
   1137       return 1;
   1138     p += rlen;
   1139   }
   1140   return 0;
   1141 }
   1142 
   1143 
   1144 /**
   1145  * Handle a Palm database that identifies itself as a MOBI book.
   1146  *
   1147  * @param ec extraction context
   1148  * @param hdr the 78 byte PDB header
   1149  */
   1150 static void
   1151 extract_mobi (struct EXTRACTOR_ExtractContext *ec,
   1152               const unsigned char *hdr)
   1153 {
   1154   unsigned char info[16];
   1155   unsigned char *rec = NULL;
   1156   uint64_t size;
   1157   uint32_t rec0_off;
   1158   uint32_t rec1_off;
   1159   uint32_t hdr_len;
   1160   uint16_t num_records;
   1161   size_t rec_len;
   1162   int have_title = 0;
   1163 
   1164   if (0 !=
   1165       ec->proc (ec->cls,
   1166                 PLUGIN_NAME,
   1167                 EXTRACTOR_METATYPE_MIMETYPE,
   1168                 EXTRACTOR_METAFORMAT_UTF8,
   1169                 "text/plain",
   1170                 "application/x-mobipocket-ebook",
   1171                 strlen ("application/x-mobipocket-ebook") + 1))
   1172     return;
   1173   if (0 !=
   1174       EXTRACTOR_forensic_emit_unix_time_ (ec,
   1175                                           PLUGIN_NAME,
   1176                                           EXTRACTOR_METATYPE_CREATION_DATE,
   1177                                           palm_time (
   1178                                             EXTRACTOR_forensic_be32_ (
   1179                                               &hdr[36]))))
   1180     return;
   1181   if (0 !=
   1182       EXTRACTOR_forensic_emit_unix_time_ (
   1183         ec,
   1184         PLUGIN_NAME,
   1185         EXTRACTOR_METATYPE_MODIFICATION_DATE,
   1186         palm_time (EXTRACTOR_forensic_be32_ (&hdr[40]))))
   1187     return;
   1188   num_records = EXTRACTOR_forensic_be16_ (&hdr[76]);
   1189   if (0 !=
   1190       EXTRACTOR_forensic_emit_ (ec,
   1191                                 PLUGIN_NAME,
   1192                                 EXTRACTOR_METATYPE_ENTRY_COUNT,
   1193                                 "%u",
   1194                                 (unsigned int) num_records))
   1195     return;
   1196   size = ec->get_size (ec->cls);
   1197   if ( (2 > num_records) ||
   1198        (UINT64_MAX == size) )
   1199     goto NAME_ONLY;
   1200   /* the record info list follows the header; entry i gives the offset
   1201      of record i, so record 0 runs up to the start of record 1 */
   1202   if (! EXTRACTOR_forensic_read_ (ec,
   1203                                   78,
   1204                                   info,
   1205                                   16))
   1206     goto NAME_ONLY;
   1207   rec0_off = EXTRACTOR_forensic_be32_ (&info[0]);
   1208   rec1_off = EXTRACTOR_forensic_be32_ (&info[8]);
   1209   if ( (rec1_off <= rec0_off) ||
   1210        (((uint64_t) rec1_off) > size) )
   1211     goto NAME_ONLY;
   1212   rec_len = (size_t) (rec1_off - rec0_off);
   1213   if (rec_len > MAX_RECORD0)
   1214     rec_len = MAX_RECORD0;
   1215   if (rec_len < 16)
   1216     goto NAME_ONLY;
   1217   if (NULL == (rec = malloc (rec_len)))
   1218     goto NAME_ONLY;
   1219   if (! EXTRACTOR_forensic_read_ (ec,
   1220                                   (int64_t) rec0_off,
   1221                                   rec,
   1222                                   rec_len))
   1223     goto NAME_ONLY;
   1224   /* PalmDOC header: encryption type at offset 12 */
   1225   switch (EXTRACTOR_forensic_be16_ (&rec[12]))
   1226   {
   1227   case 0:
   1228     break;   /* no DRM; saying so would be noise */
   1229   case 1:
   1230     if (0 !=
   1231         EXTRACTOR_forensic_emit_ (ec,
   1232                                   PLUGIN_NAME,
   1233                                   EXTRACTOR_METATYPE_ENCRYPTION,
   1234                                   "old Mobipocket encryption"))
   1235       goto CLEANUP;
   1236     break;
   1237   case 2:
   1238     if (0 !=
   1239         EXTRACTOR_forensic_emit_ (ec,
   1240                                   PLUGIN_NAME,
   1241                                   EXTRACTOR_METATYPE_ENCRYPTION,
   1242                                   "Mobipocket DRM"))
   1243       goto CLEANUP;
   1244     break;
   1245   default:
   1246     if (0 !=
   1247         EXTRACTOR_forensic_emit_ (ec,
   1248                                   PLUGIN_NAME,
   1249                                   EXTRACTOR_METATYPE_ENCRYPTION,
   1250                                   "unknown encryption type %u",
   1251                                   (unsigned int)
   1252                                   EXTRACTOR_forensic_be16_ (&rec[12])))
   1253       goto CLEANUP;
   1254     break;
   1255   }
   1256   if ( (rec_len < 32) ||
   1257        (0 != memcmp (&rec[16],
   1258                      "MOBI",
   1259                      4)) )
   1260     goto NAME_ONLY;   /* PalmDOC without a MOBI header */
   1261   hdr_len = EXTRACTOR_forensic_be32_ (&rec[20]);
   1262   if ( (hdr_len < 24) ||
   1263        (((uint64_t) hdr_len) + 16 > rec_len) )
   1264     goto NAME_ONLY;
   1265   if (hdr_len >= 28)
   1266   {
   1267     const char *cs = NULL;
   1268 
   1269     switch (EXTRACTOR_forensic_be32_ (&rec[16 + 12]))
   1270     {
   1271     case 1252: cs = "windows-1252"; break;
   1272     case 65001: cs = "UTF-8"; break;
   1273     default: break;
   1274     }
   1275     if ( (NULL != cs) &&
   1276          (0 !=
   1277           EXTRACTOR_forensic_emit_ (ec,
   1278                                     PLUGIN_NAME,
   1279                                     EXTRACTOR_METATYPE_CHARACTER_SET,
   1280                                     "%s",
   1281                                     cs)) )
   1282       goto CLEANUP;
   1283   }
   1284   /* EXTH flags live at MOBI header offset 112; bit 0x40 says that an
   1285      EXTH header follows the MOBI header */
   1286   if ( (hdr_len >= 116) &&
   1287        (0 != (EXTRACTOR_forensic_be32_ (&rec[16 + 112]) & 0x40)) )
   1288   {
   1289     if (0 !=
   1290         parse_exth (ec,
   1291                     rec,
   1292                     rec_len,
   1293                     (size_t) hdr_len + 16,
   1294                     &have_title))
   1295       goto CLEANUP;
   1296   }
   1297   if ( (! have_title) &&
   1298        (hdr_len >= 76) )
   1299   {
   1300     uint32_t fn_off = EXTRACTOR_forensic_be32_ (&rec[16 + 68]);
   1301     uint32_t fn_len = EXTRACTOR_forensic_be32_ (&rec[16 + 72]);
   1302 
   1303     if ( (fn_len > 0) &&
   1304          (fn_len <= EXTRACTOR_FORENSIC_MAX_STRING) &&
   1305          (((uint64_t) fn_off) + fn_len <= rec_len) )
   1306     {
   1307       if (0 !=
   1308           EXTRACTOR_forensic_emit_text_ (ec,
   1309                                          PLUGIN_NAME,
   1310                                          EXTRACTOR_METATYPE_TITLE,
   1311                                          (const char *) &rec[fn_off],
   1312                                          fn_len))
   1313         goto CLEANUP;
   1314       have_title = 1;
   1315     }
   1316   }
   1317 NAME_ONLY:
   1318   if (! have_title)
   1319     (void) EXTRACTOR_forensic_emit_text_ (ec,
   1320                                           PLUGIN_NAME,
   1321                                           EXTRACTOR_METATYPE_TITLE,
   1322                                           (const char *) hdr,
   1323                                           32);
   1324 CLEANUP:
   1325   free (rec);
   1326 }
   1327 
   1328 
   1329 /**
   1330  * Main entry method for the ebook extraction plugin.
   1331  *
   1332  * @param ec extraction context provided to the plugin
   1333  */
   1334 void
   1335 EXTRACTOR_ebook_extract_method (struct EXTRACTOR_ExtractContext *ec);
   1336 
   1337 void
   1338 EXTRACTOR_ebook_extract_method (struct EXTRACTOR_ExtractContext *ec)
   1339 {
   1340   unsigned char hdr[78];
   1341   struct EXTRACTOR_UnzipFile *uf;
   1342 
   1343   if (! EXTRACTOR_forensic_read_ (ec,
   1344                                   0,
   1345                                   hdr,
   1346                                   4))
   1347     return;
   1348   if (0 != memcmp (hdr,
   1349                    "PK\x03\x04",
   1350                    4))
   1351   {
   1352     /* not a ZIP; the only other thing we handle is a Palm database,
   1353        whose type/creator pair sits at offset 60 */
   1354     if (! EXTRACTOR_forensic_read_ (ec,
   1355                                     0,
   1356                                     hdr,
   1357                                     sizeof (hdr)))
   1358       return;
   1359     if (0 == memcmp (&hdr[60],
   1360                      "BOOKMOBI",
   1361                      8))
   1362       extract_mobi (ec,
   1363                     hdr);
   1364     return;
   1365   }
   1366   if (NULL == (uf = EXTRACTOR_common_unzip_open (ec)))
   1367     return;
   1368   /* a plain ZIP, a docx and an ODF file all start `PK\003\004', so
   1369      claim the file only if it carries the OCF structure */
   1370   if (has_member (uf,
   1371                   "META-INF/container.xml"))
   1372   {
   1373     extract_epub (ec,
   1374                   uf);
   1375   }
   1376   else
   1377   {
   1378     size_t len;
   1379     char *mt = read_member (uf,
   1380                             "mimetype",
   1381                             64,
   1382                             &len);
   1383 
   1384     if ( (NULL != mt) &&
   1385          (len >= strlen ("application/epub+zip")) &&
   1386          (0 == memcmp (mt,
   1387                        "application/epub+zip",
   1388                        strlen ("application/epub+zip"))) )
   1389     {
   1390       free (mt);
   1391       extract_epub (ec,
   1392                     uf);
   1393     }
   1394     else
   1395     {
   1396       free (mt);
   1397     }
   1398   }
   1399   (void) EXTRACTOR_common_unzip_close (uf);
   1400 }
   1401 
   1402 
   1403 /* end of ebook_extractor.c */