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 /* "&" 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 */