libextractor

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

test_ebook.c (12942B)


      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/test_ebook.c
     22  * @brief testcase for the ebook plugin
     23  * @author Christian Grothoff
     24  */
     25 #include "platform.h"
     26 #include "test_lib.h"
     27 
     28 
     29 /**
     30  * Shorthand for a UTF-8 text item; @a s is compared as a prefix of what
     31  * the plugin produced.
     32  */
     33 #define TXT(type, s) { type, EXTRACTOR_METAFORMAT_UTF8, "text/plain", \
     34                        s, strlen (s) + 1, 0 }
     35 
     36 
     37 /**
     38  * Check the shape of a full text: 0-terminated, no leading or trailing
     39  * white space, paragraphs separated by exactly "\n\n", no other line
     40  * breaks, no runs of spaces, no tabs.
     41  *
     42  * @param data the full text
     43  * @param data_len number of bytes in @a data, including the 0
     44  * @return 1 if the shape is right
     45  */
     46 static int
     47 fulltext_shape_ok (const char *data,
     48                    size_t data_len)
     49 {
     50   size_t len;
     51 
     52   if ( (data_len < 2) ||
     53        ('\0' != data[data_len - 1]) ||
     54        (strlen (data) != data_len - 1) )
     55     return 0;
     56   len = data_len - 1;
     57   if ( (' ' == data[0]) || ('\n' == data[0]) ||
     58        (' ' == data[len - 1]) || ('\n' == data[len - 1]) )
     59     return 0;
     60   for (size_t i = 0; i < len; i++)
     61   {
     62     if ('\t' == data[i])
     63       return 0;
     64     if ( (' ' == data[i]) &&
     65          ( (' ' == data[i + 1]) || ('\n' == data[i + 1]) ) )
     66       return 0;
     67     if ('\n' == data[i])
     68     {
     69       if ( ('\n' != data[i + 1]) ||
     70            ('\n' == data[i + 2]) ||
     71            (' ' == data[i + 2]) )
     72         return 0;
     73       i++;
     74     }
     75   }
     76   return 1;
     77 }
     78 
     79 
     80 /**
     81  * Closure for #count_cb().
     82  */
     83 struct Count
     84 {
     85   /**
     86    * Number of full text items seen.
     87    */
     88   unsigned int fulltext;
     89 
     90   /**
     91    * Number of full text items that are not normalized as promised.
     92    */
     93   unsigned int bad;
     94 };
     95 
     96 
     97 /**
     98  * Count full text items.
     99  *
    100  * @param cls a `struct Count`
    101  * @param plugin_name ignored
    102  * @param type meta data type
    103  * @param format ignored
    104  * @param data_mime_type ignored
    105  * @param data meta data
    106  * @param data_len number of bytes in @a data
    107  * @return 0 (continue)
    108  */
    109 static int
    110 count_cb (void *cls,
    111           const char *plugin_name,
    112           enum EXTRACTOR_MetaType type,
    113           enum EXTRACTOR_MetaFormat format,
    114           const char *data_mime_type,
    115           const char *data,
    116           size_t data_len)
    117 {
    118   struct Count *c = cls;
    119 
    120   if (EXTRACTOR_METATYPE_FULL_TEXT != type)
    121     return 0;
    122   c->fulltext++;
    123   if (! fulltext_shape_ok (data, data_len))
    124     c->bad++;
    125   return 0;
    126 }
    127 
    128 
    129 /**
    130  * Count the full text items the plugin reports for @a file.
    131  *
    132  * @param config plugin name with options, e.g. "ebook(fulltext)"
    133  * @param file file to extract from
    134  * @return number of full text items, UINT_MAX if one of them is
    135  *         malformed
    136  */
    137 static unsigned int
    138 count_fulltext (const char *config,
    139                 const char *file)
    140 {
    141   struct EXTRACTOR_PluginList *pl;
    142   struct Count c = { 0, 0 };
    143 
    144   pl = EXTRACTOR_plugin_add_config (NULL,
    145                                     config,
    146                                     EXTRACTOR_OPTION_DEFAULT_POLICY);
    147   EXTRACTOR_extract (pl, file, NULL, 0, &count_cb, &c);
    148   EXTRACTOR_plugin_remove_all (pl);
    149   if (0 != c.bad)
    150   {
    151     fprintf (stderr,
    152              "Full text of %s is not normalized\n",
    153              file);
    154     return UINT_MAX;
    155   }
    156   return c.fulltext;
    157 }
    158 
    159 
    160 #define EPUB_ITEMS \
    161         TXT (EXTRACTOR_METATYPE_MIMETYPE, "application/epub+zip"), \
    162         /* <package version="3.0"> */ \
    163         TXT (EXTRACTOR_METATYPE_FORMAT_VERSION, "3.0"), \
    164         TXT (EXTRACTOR_METATYPE_TITLE, "The libextractor Test Book"), \
    165         TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "Christian Grothoff"), \
    166         /* dc:contributor with opf:role="bkp" is the tool, not a person */ \
    167         TXT (EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, \
    168              "calibre (7.2.0) [https://calibre-ebook.com]"), \
    169         TXT (EXTRACTOR_METATYPE_PUBLISHER, "GNU Press"), \
    170         TXT (EXTRACTOR_METATYPE_LANGUAGE, "en-GB"), \
    171         TXT (EXTRACTOR_METATYPE_PUBLICATION_DATE, "2026-01-01T00:00:00+00:00"), \
    172         TXT (EXTRACTOR_METATYPE_KEYWORDS, "digital forensics"), \
    173         TXT (EXTRACTOR_METATYPE_KEYWORDS, "metadata extraction"), \
    174         /* the description is indented across three lines in the OPF, so \
    175            this also proves the white space collapsing */\
    176         TXT (EXTRACTOR_METATYPE_DESCRIPTION, \
    177              "A minimal EPUB built by gen_ebook_testdata.sh, used to check" \
    178              " that the ebook plugin reads Dublin Core out of the OPF" \
    179              " package document."), \
    180         /* "&amp;" has to come back as a bare ampersand */ \
    181         TXT (EXTRACTOR_METATYPE_RIGHTS, "Public domain (CC0) & nothing else"), \
    182         /* dc:identifier with opf:scheme="ISBN" */ \
    183         TXT (EXTRACTOR_METATYPE_ISBN, "978-3-16-148410-0"), \
    184         /* ... and the one that is not an ISBN */ \
    185         TXT (EXTRACTOR_METATYPE_URI, \
    186              "urn:uuid:0f9d5a1e-1719-4c0b-9a3e-000000001719"), \
    187         TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2026-01-01T00:00:00+00:00"), \
    188         TXT (EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, "gen_ebook_testdata.sh"), \
    189         TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2026-02-01T00:00:00Z"), \
    190         /* seven <item> entries in the manifest, and not the three \
    191            <itemref> entries in the spine */\
    192         TXT (EXTRACTOR_METATYPE_ENTRY_COUNT, "7"), \
    193         TXT (EXTRACTOR_METATYPE_ENCRYPTION, \
    194              "META-INF/encryption.xml present (DRM or font obfuscation)")
    195 
    196 #define MOBI_ITEMS \
    197         TXT (EXTRACTOR_METATYPE_MIMETYPE, "application/x-mobipocket-ebook"), \
    198         /* PDB header offsets 36/40, seconds since 1904 */ \
    199         TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2026-01-01T00:00:00Z"), \
    200         TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2026-02-01T00:00:00Z"), \
    201         /* ... and the same moment again, from EXTH 502 */ \
    202         TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2026-02-01T00:00:00+00:00"), \
    203         TXT (EXTRACTOR_METATYPE_ENTRY_COUNT, "3"), \
    204         /* PalmDOC header offset 12 */ \
    205         TXT (EXTRACTOR_METATYPE_ENCRYPTION, "old Mobipocket encryption"), \
    206         /* MOBI header offset 28 */ \
    207         TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "UTF-8"), \
    208         TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "Christian Grothoff"), \
    209         TXT (EXTRACTOR_METATYPE_PUBLISHER, "GNU Press"), \
    210         TXT (EXTRACTOR_METATYPE_DESCRIPTION, \
    211              "A hand-built minimal MOBI file used by the libextractor" \
    212              " test suite."), \
    213         TXT (EXTRACTOR_METATYPE_ISBN, "978-3-16-148410-0"), \
    214         TXT (EXTRACTOR_METATYPE_KEYWORDS, "digital forensics"), \
    215         TXT (EXTRACTOR_METATYPE_PUBLICATION_DATE, "2026-01-01"), \
    216         TXT (EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, \
    217              "calibre (7.2.0) [https://calibre-ebook.com]"), \
    218         TXT (EXTRACTOR_METATYPE_RIGHTS, "Public domain (CC0)"), \
    219         TXT (EXTRACTOR_METATYPE_SERIAL, "B00LIBEXTR7"), \
    220         TXT (EXTRACTOR_METATYPE_WATERMARK, "libextractor-test-watermark"), \
    221         TXT (EXTRACTOR_METATYPE_ATTRIBUTES, "cdetype: EBOK"), \
    222         /* EXTH 503, which is why the PDB database name \
    223            "The_libextractor_Test_Book" is not reported as the title */\
    224         TXT (EXTRACTOR_METATYPE_TITLE, "The libextractor Test Book"), \
    225         TXT (EXTRACTOR_METATYPE_LANGUAGE, "en-gb")
    226 
    227 /**
    228  * Main function for the ebook testcase.
    229  *
    230  * @param argc number of arguments (ignored)
    231  * @param argv arguments (ignored)
    232  * @return 0 on success
    233  */
    234 int
    235 main (int argc, char *argv[])
    236 {
    237   struct SolutionData epub_sol[] = {
    238     EPUB_ITEMS,
    239     { 0, 0, NULL, NULL, 0, -1 }
    240   };
    241   struct SolutionData mobi_sol[] = {
    242     MOBI_ITEMS,
    243     { 0, 0, NULL, NULL, 0, -1 }
    244   };
    245 #define EPUB_FT_ITEMS \
    246         TXT (EXTRACTOR_METATYPE_MIMETYPE, "application/epub+zip"), \
    247         TXT (EXTRACTOR_METATYPE_FORMAT_VERSION, "3.0"), \
    248         TXT (EXTRACTOR_METATYPE_TITLE, "Full Text Test"), \
    249         TXT (EXTRACTOR_METATYPE_URI, \
    250              "urn:uuid:0f9d5a1e-1719-4c0b-9a3e-00000000f7f7"), \
    251         TXT (EXTRACTOR_METATYPE_LANGUAGE, "en"), \
    252         TXT (EXTRACTOR_METATYPE_ENTRY_COUNT, "7"), \
    253         TXT (EXTRACTOR_METATYPE_ENCRYPTION, \
    254              "META-INF/encryption.xml present (DRM or font obfuscation)")
    255 #define MOBI_FT_ITEMS \
    256         TXT (EXTRACTOR_METATYPE_MIMETYPE, "application/x-mobipocket-ebook"), \
    257         TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2026-01-01T00:00:00Z"), \
    258         TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2026-02-01T00:00:00Z"), \
    259         TXT (EXTRACTOR_METATYPE_ENTRY_COUNT, "8"), \
    260         TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "UTF-8"), \
    261         TXT (EXTRACTOR_METATYPE_TITLE, "Full Text Test")
    262   struct SolutionData epub_ft_sol[] = {
    263     EPUB_FT_ITEMS,
    264     { 0, 0, NULL, NULL, 0, -1 }
    265   };
    266   struct SolutionData mobi_ft_sol[] = {
    267     MOBI_FT_ITEMS,
    268     { 0, 0, NULL, NULL, 0, -1 }
    269   };
    270   struct ProblemSet ps[] = {
    271     { "testdata/ebook_test.epub", epub_sol },
    272     { "testdata/ebook_test.mobi", mobi_sol },
    273     { "testdata/ebook_fulltext.epub", epub_ft_sol },
    274     { "testdata/ebook_fulltext.mobi", mobi_ft_sol },
    275     { NULL, NULL }
    276   };
    277   /* The spine order is nav, c3, c1, (secret,) c2, (notes): the
    278      navigation document contributes nothing, the DRM-protected
    279      `secret' and the linear="no" `notes' are skipped; c1 is
    280      ISO-8859-1; page-level header/footer, the footnote aside, the
    281      page break marker, hidden elements, head, style and script are
    282      dropped. */
    283 #define EPUB_TEXT \
    284         "Chapter Alpha\n\n" \
    285         "Alpha text comes first in the reading order.\n\n" \
    286         "Second paragraph of alpha\xe2\x80\x94with a dash & an ampersand.\n\n" \
    287         "Chapter Beta\n\n" \
    288         "Beta: caf\xc3\xa9 cr\xc3\xa8me.*\n\n" \
    289         "Chapter Gamma\n\n" \
    290         "Gamma line one line two\n\n" \
    291         "Gamma new paragraph here.\n\n" \
    292         "Last\xe2\x80\xa6 really last."
    293   /* PalmDOC compressed, with trailing entries and a character that
    294      straddles two records; <mbp:pagebreak/> ends a paragraph */
    295 #define MOBI_TEXT \
    296         "The Test Book\n\n" \
    297         "First paragraph with an \xc3\xbcmlaut, repeated text, repeated text," \
    298         " repeated text.\n\n" \
    299         "Second chapter starts here \xe2\x80\x94 \xc3\xa0 la carte.\n\n" \
    300         "Line break and & entity."
    301   /* the chapters of ebook_test.epub in spine order */
    302 #define EPUB_SIMPLE_TEXT \
    303         "One\n\nNothing to see here.\n\n" \
    304         "Two\n\nNothing to see here.\n\n" \
    305         "Three\n\nNothing to see here."
    306   /* with the option: the same items, plus the full text */
    307   struct SolutionData epub_sol_ft[] = {
    308     EPUB_ITEMS,
    309     TXT (EXTRACTOR_METATYPE_FULL_TEXT, EPUB_SIMPLE_TEXT),
    310     { 0, 0, NULL, NULL, 0, -1 }
    311   };
    312   struct SolutionData epub_ft_sol_ft[] = {
    313     EPUB_FT_ITEMS,
    314     TXT (EXTRACTOR_METATYPE_FULL_TEXT, EPUB_TEXT),
    315     { 0, 0, NULL, NULL, 0, -1 }
    316   };
    317   struct SolutionData mobi_ft_sol_ft[] = {
    318     MOBI_FT_ITEMS,
    319     TXT (EXTRACTOR_METATYPE_FULL_TEXT, MOBI_TEXT),
    320     { 0, 0, NULL, NULL, 0, -1 }
    321   };
    322   struct ProblemSet ps_ft[] = {
    323     { "testdata/ebook_test.epub", epub_sol_ft },
    324     /* ebook_test.mobi is flagged as encrypted: no full text */
    325     { "testdata/ebook_test.mobi", mobi_sol },
    326     { "testdata/ebook_fulltext.epub", epub_ft_sol_ft },
    327     { "testdata/ebook_fulltext.mobi", mobi_ft_sol_ft },
    328     { NULL, NULL }
    329   };
    330   static const char *const files[] = {
    331     "testdata/ebook_test.epub",
    332     "testdata/ebook_test.mobi",
    333     "testdata/ebook_fulltext.epub",
    334     "testdata/ebook_fulltext.mobi",
    335     NULL
    336   };
    337   int ret;
    338 
    339   if (0 != (ret = ET_main ("ebook", ps)))
    340     return ret;
    341   if (0 != (ret = ET_main ("ebook(fulltext)", ps_ft)))
    342     return ret;
    343   /* ET_main only warns about extra items: make sure that the full text
    344      appears exactly when it was asked for (and not for the encrypted
    345      MOBI) */
    346   for (unsigned int i = 0; NULL != files[i]; i++)
    347   {
    348     unsigned int want = (1 == i) ? 0 : 1;
    349 
    350     if ( (0 != count_fulltext ("ebook", files[i])) ||
    351          (want != count_fulltext ("ebook(fulltext)", files[i])) )
    352     {
    353       fprintf (stderr,
    354                "Wrong number of full text items for %s\n",
    355                files[i]);
    356       return 1;
    357     }
    358   }
    359   return 0;
    360 }
    361 
    362 
    363 /* end of test_ebook.c */