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