msoffice_extractor.c (32844B)
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/msoffice_extractor.c 22 * @brief plugin to support Microsoft Office documents that are not 23 * stored in an OLE2 container 24 * @author Christian Grothoff 25 * 26 * The `ole2' plugin covers the OLE2-based generation of Microsoft 27 * Office formats (.doc, .xls, .ppt) and needs libgsf to open the 28 * container. This plugin covers the two families that live outside of 29 * an OLE2 container and hence need no libgsf at all: 30 * 31 * - Office Open XML (.docx, .xlsx, .pptx and their macro-enabled and 32 * template variants), which is a ZIP archive of XML parts. 33 * - Bare BIFF record streams, which is how Excel 2 to Excel 4 wrote 34 * .xls files before OLE2 existed. These are still common. 35 * 36 * Beyond the document summary, both carry a considerable amount of 37 * information about *who* edited a document and *when*: tracked 38 * changes, comment authors, the identities used to author them, 39 * shared-workbook revision logs, the "send for review" e-mail 40 * addresses left behind by Outlook, and the user name recorded in the 41 * Excel file protection records. Extracting this is the point of 42 * https://bugs.gnunet.org/view.php?id=2096 -- it matters both for 43 * forensics and for privacy tools that want to warn about the 44 * information a document leaks. 45 */ 46 #include "platform.h" 47 #include <ctype.h> 48 #include "extractor.h" 49 #include "convert.h" 50 #include "unzip.h" 51 #include "msoffice_biff.h" 52 53 54 /** 55 * Name this plugin reports itself as. 56 */ 57 #define PLUGIN_NAME "msoffice" 58 59 /** 60 * Maximum length of a file name inside the ZIP archive. 61 */ 62 #define MAXFILENAME 256 63 64 /** 65 * Maximum number of interesting ZIP entries we remember. Documents 66 * with more comment or revision parts than this are pathological. 67 */ 68 #define MAX_PARTS 64 69 70 /** 71 * Maximum size of a part that we read into memory as a whole. Only 72 * used for the small property parts. 73 */ 74 #define MAX_SMALL_PART (256 * 1024) 75 76 /** 77 * Size of the sliding window used to scan the large parts. 78 */ 79 #define SCAN_CHUNK (128 * 1024) 80 81 /** 82 * Longest XML tag we are able to look at in one piece. Tags carrying 83 * an author are far shorter; anything longer is skipped rather than 84 * buffered, which bounds our memory use on hostile input. 85 */ 86 #define MAX_TAG 8192 87 88 /** 89 * Maximum number of bytes we scan in a single part. Guards against 90 * decompression bombs. 91 */ 92 #define MAX_SCAN_BYTES (64 * 1024 * 1024) 93 94 /** 95 * Maximum number of distinct strings we remember to suppress 96 * duplicates. A document with tracked changes typically repeats the 97 * same handful of authors thousands of times. 98 */ 99 #define MAX_SEEN 256 100 101 /** 102 * Longest metadata value we report. 103 */ 104 #define MAX_VALUE 4096 105 106 107 /** 108 * State kept while extracting from one document. 109 */ 110 struct MsoContext 111 { 112 /** 113 * Extraction context we were called with. 114 */ 115 struct EXTRACTOR_ExtractContext *ec; 116 117 /** 118 * Strings we have already reported, to suppress duplicates. 119 */ 120 char *seen[MAX_SEEN]; 121 122 /** 123 * Number of used entries in @e seen. 124 */ 125 unsigned int seen_len; 126 127 /** 128 * Set to 1 once the caller asked us to stop. 129 */ 130 int stop; 131 }; 132 133 134 /** 135 * Entry in a map from XML element names to LE meta data types. 136 */ 137 struct Matches 138 { 139 /** 140 * Name of the XML element. 141 */ 142 const char *text; 143 144 /** 145 * Corresponding LE type. 146 */ 147 enum EXTRACTOR_MetaType type; 148 }; 149 150 151 /** 152 * Elements of the OPC core properties part (`docProps/core.xml'), 153 * which is the Dublin Core derived summary of the document. 154 */ 155 static struct Matches core_map[] = { 156 { "dc:title", EXTRACTOR_METATYPE_TITLE }, 157 { "dc:subject", EXTRACTOR_METATYPE_SUBJECT }, 158 { "dc:creator", EXTRACTOR_METATYPE_CREATOR }, 159 { "dc:description", EXTRACTOR_METATYPE_DESCRIPTION }, 160 { "dc:language", EXTRACTOR_METATYPE_LANGUAGE }, 161 { "cp:keywords", EXTRACTOR_METATYPE_KEYWORDS }, 162 { "cp:category", EXTRACTOR_METATYPE_SECTION }, 163 { "cp:lastModifiedBy", EXTRACTOR_METATYPE_LAST_SAVED_BY }, 164 { "cp:revision", EXTRACTOR_METATYPE_REVISION_NUMBER }, 165 { "cp:lastPrinted", EXTRACTOR_METATYPE_LAST_PRINTED }, 166 { "dcterms:created", EXTRACTOR_METATYPE_CREATION_DATE }, 167 { "dcterms:modified", EXTRACTOR_METATYPE_MODIFICATION_DATE }, 168 { NULL, 0 } 169 }; 170 171 172 /** 173 * Elements of the extended properties part (`docProps/app.xml'), 174 * which is written by the application that saved the document. 175 */ 176 static struct Matches app_map[] = { 177 { "Application", EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE }, 178 { "AppVersion", EXTRACTOR_METATYPE_SOFTWARE_VERSION }, 179 { "Company", EXTRACTOR_METATYPE_COMPANY }, 180 { "Manager", EXTRACTOR_METATYPE_MANAGER }, 181 { "Template", EXTRACTOR_METATYPE_TEMPLATE }, 182 { "TotalTime", EXTRACTOR_METATYPE_TOTAL_EDITING_TIME }, 183 { "Pages", EXTRACTOR_METATYPE_PAGE_COUNT }, 184 { "Words", EXTRACTOR_METATYPE_WORD_COUNT }, 185 { "Characters", EXTRACTOR_METATYPE_CHARACTER_COUNT }, 186 { "Lines", EXTRACTOR_METATYPE_LINE_COUNT }, 187 { "Paragraphs", EXTRACTOR_METATYPE_PARAGRAPH_COUNT }, 188 { NULL, 0 } 189 }; 190 191 192 /** 193 * Custom properties (`docProps/custom.xml') that carry personal 194 * information. Word writes these when a document is sent around for 195 * review from Outlook; they routinely outlive the review and identify 196 * the person who circulated the document. 197 */ 198 static struct Matches custom_map[] = { 199 { "_AuthorEmail", EXTRACTOR_METATYPE_AUTHOR_EMAIL }, 200 { "_AuthorEmailDisplayName", EXTRACTOR_METATYPE_AUTHOR_NAME }, 201 { "_EmailSubject", EXTRACTOR_METATYPE_SUBJECT }, 202 { NULL, 0 } 203 }; 204 205 206 /* ******************** generic helpers ******************** */ 207 208 209 /** 210 * Trim leading and trailing white space in @a s, in place. 211 * 212 * @param s 0-terminated string to trim 213 * @return pointer into @a s to the first non-blank character 214 */ 215 static char * 216 trim (char *s) 217 { 218 size_t len = strlen (s); 219 220 while ( (0 < len) && 221 (isspace ((unsigned char) s[len - 1])) ) 222 s[--len] = '\0'; 223 while (isspace ((unsigned char) s[0])) 224 s++; 225 return s; 226 } 227 228 229 /** 230 * Check whether @a value was reported before, and remember it if not. 231 * 232 * @param mc our extraction state 233 * @param value string to check 234 * @return 1 if @a value is new (and was remembered), 0 if it is a 235 * duplicate or if we ran out of space to remember it 236 */ 237 static int 238 mark_seen (struct MsoContext *mc, 239 const char *value) 240 { 241 unsigned int i; 242 char *dup; 243 244 for (i = 0; i < mc->seen_len; i++) 245 if (0 == strcmp (mc->seen[i], value)) 246 return 0; 247 if (MAX_SEEN == mc->seen_len) 248 return 0; 249 if (NULL == (dup = strdup (value))) 250 return 0; 251 mc->seen[mc->seen_len++] = dup; 252 return 1; 253 } 254 255 256 /** 257 * Report a meta data value, unless it is empty or a duplicate. 258 * 259 * @param mc our extraction state 260 * @param type meta data type to report the value as 261 * @param value the value; leading and trailing white space is removed 262 */ 263 static void 264 add_meta (struct MsoContext *mc, 265 enum EXTRACTOR_MetaType type, 266 const char *value) 267 { 268 char *tmp; 269 char *val; 270 char key[64]; 271 272 if ( (0 != mc->stop) || 273 (NULL == value) ) 274 return; 275 if (MAX_VALUE < strlen (value)) 276 return; 277 if (NULL == (tmp = strdup (value))) 278 return; 279 val = trim (tmp); 280 if ('\0' == val[0]) 281 { 282 free (tmp); 283 return; 284 } 285 /* De-duplicate per type: the same name legitimately shows up as both 286 a creator and a comment author, and both are worth reporting. */ 287 if ( (0 < snprintf (key, sizeof (key), "%d:", (int) type)) && 288 (strlen (key) + strlen (val) < MAX_VALUE) ) 289 { 290 char full[MAX_VALUE + 64]; 291 292 snprintf (full, sizeof (full), "%d:%s", (int) type, val); 293 if (0 == mark_seen (mc, full)) 294 { 295 free (tmp); 296 return; 297 } 298 } 299 if (0 != mc->ec->proc (mc->ec->cls, 300 PLUGIN_NAME, 301 type, 302 EXTRACTOR_METAFORMAT_UTF8, 303 "text/plain", 304 val, 305 strlen (val) + 1)) 306 mc->stop = 1; 307 free (tmp); 308 } 309 310 311 /* ******************** minimal XML scanning ******************** */ 312 313 314 /** 315 * Append the UTF-8 encoding of @a cp to @a out at @a *off. 316 * 317 * @param out buffer to write to 318 * @param off offset into @a out, updated 319 * @param cp code point to encode 320 */ 321 static void 322 append_utf8 (char *out, 323 size_t *off, 324 unsigned int cp) 325 { 326 size_t o = *off; 327 328 if (cp < 0x80) 329 { 330 out[o++] = (char) cp; 331 } 332 else if (cp < 0x800) 333 { 334 out[o++] = (char) (0xC0 | (cp >> 6)); 335 out[o++] = (char) (0x80 | (cp & 0x3F)); 336 } 337 else if (cp < 0x10000) 338 { 339 out[o++] = (char) (0xE0 | (cp >> 12)); 340 out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F)); 341 out[o++] = (char) (0x80 | (cp & 0x3F)); 342 } 343 else 344 { 345 out[o++] = (char) (0xF0 | (cp >> 18)); 346 out[o++] = (char) (0x80 | ((cp >> 12) & 0x3F)); 347 out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F)); 348 out[o++] = (char) (0x80 | (cp & 0x3F)); 349 } 350 *off = o; 351 } 352 353 354 /** 355 * Resolve the XML entity starting at @a in (which points at the `&'). 356 * 357 * @param in start of the entity 358 * @param len number of bytes available at @a in 359 * @param out buffer to append the replacement text to 360 * @param off offset into @a out, updated on success 361 * @return number of bytes consumed from @a in, 0 if this is not an 362 * entity reference we understand 363 */ 364 static size_t 365 resolve_entity (const char *in, 366 size_t len, 367 char *out, 368 size_t *off) 369 { 370 static const struct 371 { 372 const char *name; 373 char value; 374 } named[] = { 375 { "amp;", '&' }, 376 { "lt;", '<' }, 377 { "gt;", '>' }, 378 { "quot;", '"' }, 379 { "apos;", '\'' }, 380 { NULL, 0 } 381 }; 382 unsigned int i; 383 unsigned long cp; 384 size_t n; 385 386 for (i = 0; NULL != named[i].name; i++) 387 { 388 size_t nl = strlen (named[i].name); 389 390 if ( (len > nl) && 391 (0 == strncmp (in + 1, named[i].name, nl)) ) 392 { 393 out[(*off)++] = named[i].value; 394 return nl + 1; 395 } 396 } 397 if ( (2 < len) && 398 ('#' == in[1]) ) 399 { 400 char *end; 401 402 if ( ('x' == in[2]) || 403 ('X' == in[2]) ) 404 cp = strtoul (in + 3, &end, 16); 405 else 406 cp = strtoul (in + 2, &end, 10); 407 n = (size_t) (end - in); 408 if ( (end > in + 2) && 409 (n < len) && 410 (';' == *end) && 411 (0 < cp) && 412 (0x10FFFF >= cp) ) 413 { 414 append_utf8 (out, off, (unsigned int) cp); 415 return n + 1; 416 } 417 } 418 return 0; 419 } 420 421 422 /** 423 * Copy @a len bytes from @a in to a fresh 0-terminated string, 424 * resolving XML entity references on the way. OOXML parts are UTF-8, 425 * so no character set conversion is needed. 426 * 427 * @param in text to unescape 428 * @param len number of bytes in @a in 429 * @return the unescaped string to be freed by the caller, NULL on 430 * error or if @a len exceeds what we are willing to report 431 */ 432 static char * 433 xml_unescape (const char *in, 434 size_t len) 435 { 436 char *out; 437 size_t i = 0; 438 size_t o = 0; 439 440 if (MAX_VALUE < len) 441 return NULL; 442 /* A resolved numeric reference is never longer than its source. */ 443 if (NULL == (out = malloc (len + 1))) 444 return NULL; 445 while (i < len) 446 { 447 if ('&' == in[i]) 448 { 449 size_t used = resolve_entity (&in[i], len - i, out, &o); 450 451 if (0 != used) 452 { 453 i += used; 454 continue; 455 } 456 } 457 out[o++] = in[i++]; 458 } 459 out[o] = '\0'; 460 return out; 461 } 462 463 464 /** 465 * Locate the value of attribute @a attr inside the XML tag @a tag. 466 * 467 * @param tag start of the tag (at the `<') 468 * @param len length of the tag, including the `>' 469 * @param attr name of the attribute to look for 470 * @return the unescaped attribute value to be freed by the caller, 471 * NULL if the attribute is not present 472 */ 473 static char * 474 tag_attr (const char *tag, 475 size_t len, 476 const char *attr) 477 { 478 size_t alen = strlen (attr); 479 size_t i; 480 481 for (i = 1; i + alen + 2 < len; i++) 482 { 483 const char *q; 484 const char *end; 485 486 /* The attribute name must be preceded by white space, so that 487 looking for "name" does not match "displayName". */ 488 if (! isspace ((unsigned char) tag[i - 1])) 489 continue; 490 if (0 != strncmp (&tag[i], attr, alen)) 491 continue; 492 q = &tag[i + alen]; 493 while ( (q < tag + len) && 494 (isspace ((unsigned char) *q)) ) 495 q++; 496 if ( (q >= tag + len) || 497 ('=' != *q) ) 498 continue; 499 q++; 500 while ( (q < tag + len) && 501 (isspace ((unsigned char) *q)) ) 502 q++; 503 if ( (q >= tag + len) || 504 ( ('"' != *q) && 505 ('\'' != *q) ) ) 506 continue; 507 end = memchr (q + 1, *q, (size_t) (tag + len - q - 1)); 508 if (NULL == end) 509 return NULL; 510 return xml_unescape (q + 1, (size_t) (end - q - 1)); 511 } 512 return NULL; 513 } 514 515 516 /** 517 * Return the value of the first attribute from @a attrs that is 518 * present in @a tag. 519 * 520 * @param tag start of the tag (at the `<') 521 * @param len length of the tag, including the `>' 522 * @param attrs NULL-terminated array of attribute names, in order of 523 * preference 524 * @return the unescaped attribute value to be freed by the caller, 525 * NULL if none of the attributes is present 526 */ 527 static char * 528 tag_attr_any (const char *tag, 529 size_t len, 530 const char **attrs) 531 { 532 unsigned int i; 533 534 for (i = 0; NULL != attrs[i]; i++) 535 { 536 char *v = tag_attr (tag, len, attrs[i]); 537 538 if (NULL != v) 539 return v; 540 } 541 return NULL; 542 } 543 544 545 /** 546 * Check whether the local name of the element in @a tag equals 547 * @a name, ignoring any XML namespace prefix and case. 548 * 549 * @param tag start of the tag (at the `<') 550 * @param len length of the tag, including the `>' 551 * @param name local element name to compare against 552 * @return 1 on match, 0 otherwise 553 */ 554 static int 555 tag_local_name_is (const char *tag, 556 size_t len, 557 const char *name) 558 { 559 size_t start = 1; 560 size_t i; 561 size_t nlen = strlen (name); 562 563 for (i = 1; i < len; i++) 564 { 565 if (':' == tag[i]) 566 start = i + 1; 567 if ( (isspace ((unsigned char) tag[i])) || 568 ('/' == tag[i]) || 569 ('>' == tag[i]) ) 570 break; 571 } 572 if (i - start != nlen) 573 return 0; 574 for (i = 0; i < nlen; i++) 575 if (tolower ((unsigned char) tag[start + i]) != tolower ((unsigned char) 576 name[i])) 577 return 0; 578 return 1; 579 } 580 581 582 /** 583 * Extract the text content of every occurrence of element @a name in 584 * @a buf and report it as @a type. 585 * 586 * @param mc our extraction state 587 * @param buf 0-terminated XML document 588 * @param name element name to look for, including any namespace prefix 589 * @param type meta data type to report the content as 590 */ 591 static void 592 report_element (struct MsoContext *mc, 593 const char *buf, 594 const char *name, 595 enum EXTRACTOR_MetaType type) 596 { 597 char open[128]; 598 const char *p = buf; 599 size_t nlen = strlen (name); 600 601 if (sizeof (open) - 2 <= nlen) 602 return; 603 open[0] = '<'; 604 memcpy (&open[1], name, nlen + 1); 605 while (0 == mc->stop) 606 { 607 const char *start; 608 const char *gt; 609 const char *close; 610 char *value; 611 612 if (NULL == (p = strstr (p, open))) 613 return; 614 start = p + 1 + nlen; 615 /* The match must end the element name, not just prefix it. */ 616 if ( ('>' != start[0]) && 617 ('/' != start[0]) && 618 (! isspace ((unsigned char) start[0])) ) 619 { 620 p = start; 621 continue; 622 } 623 if (NULL == (gt = strchr (start, '>'))) 624 return; 625 p = gt + 1; 626 if ('/' == gt[-1]) 627 continue; /* empty element */ 628 if (NULL == (close = strstr (p, "</"))) 629 return; 630 if (NULL != (value = xml_unescape (p, (size_t) (close - p)))) 631 { 632 add_meta (mc, type, value); 633 free (value); 634 } 635 p = close; 636 } 637 } 638 639 640 /** 641 * Report the value of the custom document property @a name, if 642 * present. A custom property looks like 643 * `<property ... name="_AuthorEmail"><vt:lpwstr>a@b</vt:lpwstr></property>'. 644 * 645 * @param mc our extraction state 646 * @param buf 0-terminated XML document 647 * @param name value of the `name' attribute to look for 648 * @param type meta data type to report the value as 649 */ 650 static void 651 report_custom_property (struct MsoContext *mc, 652 const char *buf, 653 const char *name, 654 enum EXTRACTOR_MetaType type) 655 { 656 char needle[128]; 657 const char *p; 658 const char *gt; 659 const char *lt; 660 char *value; 661 662 if (sizeof (needle) <= strlen (name) + 8) 663 return; 664 snprintf (needle, sizeof (needle), "name=\"%s\"", name); 665 if (NULL == (p = strstr (buf, needle))) 666 return; 667 /* Skip to the end of the <property> tag, then into the typed value 668 element that follows. */ 669 if (NULL == (gt = strchr (p, '>'))) 670 return; 671 if (NULL == (p = strchr (gt + 1, '<'))) 672 return; 673 if (NULL == (gt = strchr (p, '>'))) 674 return; 675 if ('/' == gt[-1]) 676 return; 677 if (NULL == (lt = strchr (gt + 1, '<'))) 678 return; 679 if (NULL != (value = xml_unescape (gt + 1, (size_t) (lt - gt - 1)))) 680 { 681 add_meta (mc, type, value); 682 free (value); 683 } 684 } 685 686 687 /* ******************** who edited the document ******************** */ 688 689 690 /** 691 * Attributes that carry the name of a person, in order of preference. 692 * `w:author' is used by Word for tracked changes and comments, 693 * `w15:author' by the Word 2013 people part, `userName' by the Excel 694 * shared workbook revision log and `displayName' by the Excel persons 695 * part. 696 */ 697 static const char *author_attrs[] = { 698 "w:author", 699 "w15:author", 700 "userName", 701 "displayName", 702 NULL 703 }; 704 705 706 /** 707 * Attributes that carry the time of an edit. 708 */ 709 static const char *date_attrs[] = { 710 "w:date", 711 "w15:date", 712 "dateTime", 713 "p:dt", 714 NULL 715 }; 716 717 718 /** 719 * Attributes that carry a directory identity -- an Active Directory 720 * SID or, more often, the e-mail address the editor was signed in as. 721 */ 722 static const char *userid_attrs[] = { 723 "w15:userId", 724 "userId", 725 NULL 726 }; 727 728 729 /** 730 * Inspect one XML tag for information about who edited the document. 731 * 732 * This deliberately keys off the attributes rather than off a list of 733 * element names: Word alone marks up revisions with more than a dozen 734 * different elements (`w:ins', `w:del', `w:moveFrom', `w:rPrChange', 735 * `w:tcPrChange', ...), and they all carry the author and date the 736 * same way. 737 * 738 * @param mc our extraction state 739 * @param tag start of the tag (at the `<') 740 * @param len length of the tag, including the `>' 741 */ 742 static void 743 handle_tag (struct MsoContext *mc, 744 const char *tag, 745 size_t len) 746 { 747 char *author; 748 char *date; 749 char *userid; 750 751 if ( (2 > len) || 752 ('/' == tag[1]) || 753 ('?' == tag[1]) || 754 ('!' == tag[1]) ) 755 return; 756 author = tag_attr_any (tag, len, author_attrs); 757 if (NULL == author) 758 { 759 /* PowerPoint keeps its comment authors in a list of <p:cmAuthor> 760 (and, since PowerPoint 2016, <p188:author>) elements that name 761 the person in a plain `name' attribute. */ 762 if ( (tag_local_name_is (tag, len, "cmAuthor")) || 763 (tag_local_name_is (tag, len, "author")) || 764 (tag_local_name_is (tag, len, "person")) ) 765 author = tag_attr (tag, len, "name"); 766 } 767 userid = tag_attr_any (tag, len, userid_attrs); 768 if ( (NULL == author) && 769 (NULL == userid) ) 770 return; 771 date = tag_attr_any (tag, len, date_attrs); 772 if (NULL != author) 773 add_meta (mc, EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, author); 774 if (NULL != userid) 775 add_meta (mc, EXTRACTOR_METATYPE_CONTACT_INFORMATION, userid); 776 if ( (NULL != author) && 777 (NULL != date) ) 778 { 779 char *line; 780 size_t bsize = strlen (author) + strlen (date) + 128; 781 782 if (NULL != (line = malloc (bsize))) 783 { 784 int sret = snprintf (line, 785 bsize, 786 _ ("Author `%s' edited the document on `%s'"), 787 author, 788 date); 789 790 if ( (0 < sret) && 791 (bsize > (size_t) sret) ) 792 add_meta (mc, EXTRACTOR_METATYPE_REVISION_HISTORY, line); 793 free (line); 794 } 795 } 796 free (author); 797 free (date); 798 free (userid); 799 } 800 801 802 /** 803 * Scan a buffer of XML for tags and hand each of them to 804 * #handle_tag(). 805 * 806 * @param mc our extraction state 807 * @param buf buffer to scan 808 * @param len number of bytes in @a buf 809 * @return number of bytes consumed; the remainder is an incomplete tag 810 * that the caller should present again with more data 811 */ 812 static size_t 813 scan_tags (struct MsoContext *mc, 814 const char *buf, 815 size_t len) 816 { 817 const char *p = buf; 818 const char *end = buf + len; 819 820 while (p < end) 821 { 822 const char *lt; 823 const char *gt; 824 825 if (NULL == (lt = memchr (p, '<', (size_t) (end - p)))) 826 return len; 827 if (NULL == (gt = memchr (lt, '>', (size_t) (end - lt)))) 828 return (size_t) (lt - buf); 829 handle_tag (mc, lt, (size_t) (gt - lt + 1)); 830 if (0 != mc->stop) 831 return len; 832 p = gt + 1; 833 } 834 return len; 835 } 836 837 838 /* ******************** reading ZIP parts ******************** */ 839 840 841 /** 842 * Read the currently selected ZIP entry into a fresh 0-terminated 843 * buffer. 844 * 845 * @param uf the ZIP archive 846 * @param max maximum number of bytes to read 847 * @param size set to the number of bytes read 848 * @return the buffer to be freed by the caller, NULL on error 849 */ 850 static char * 851 read_current (struct EXTRACTOR_UnzipFile *uf, 852 size_t max, 853 size_t *size) 854 { 855 struct EXTRACTOR_UnzipFileInfo fi; 856 char *buf; 857 ssize_t got; 858 size_t want; 859 860 if (EXTRACTOR_UNZIP_OK != 861 EXTRACTOR_common_unzip_get_current_file_info (uf, &fi, NULL, 0, 862 NULL, 0, NULL, 0)) 863 return NULL; 864 want = fi.uncompressed_size; 865 if (want > max) 866 want = max; 867 if (0 == want) 868 return NULL; 869 if (EXTRACTOR_UNZIP_OK != 870 EXTRACTOR_common_unzip_open_current_file (uf)) 871 return NULL; 872 if (NULL == (buf = malloc (want + 1))) 873 { 874 EXTRACTOR_common_unzip_close_current_file (uf); 875 return NULL; 876 } 877 got = EXTRACTOR_common_unzip_read_current_file (uf, buf, want); 878 EXTRACTOR_common_unzip_close_current_file (uf); 879 if (0 >= got) 880 { 881 free (buf); 882 return NULL; 883 } 884 buf[got] = '\0'; 885 *size = (size_t) got; 886 return buf; 887 } 888 889 890 /** 891 * Read the part @a name into memory and return it 0-terminated. 892 * 893 * @param uf the ZIP archive 894 * @param name name of the part 895 * @return the buffer to be freed by the caller, NULL if the part is 896 * absent, empty or too large 897 */ 898 static char * 899 read_part (struct EXTRACTOR_UnzipFile *uf, 900 const char *name) 901 { 902 size_t size; 903 904 if (EXTRACTOR_UNZIP_OK != 905 EXTRACTOR_common_unzip_go_find_local_file (uf, name, 2)) 906 return NULL; 907 return read_current (uf, MAX_SMALL_PART, &size); 908 } 909 910 911 /** 912 * Scan the part @a name for tags describing who edited the document. 913 * The part is read through a sliding window, so that a part far too 914 * large to hold in memory (`word/document.xml' of a heavily revised 915 * document, or a decompression bomb) costs us a bounded amount of 916 * memory. 917 * 918 * @param mc our extraction state 919 * @param uf the ZIP archive 920 * @param name name of the part to scan 921 */ 922 static void 923 scan_part (struct MsoContext *mc, 924 struct EXTRACTOR_UnzipFile *uf, 925 const char *name) 926 { 927 char *buf; 928 size_t carry = 0; 929 uint64_t total = 0; 930 931 if (EXTRACTOR_UNZIP_OK != 932 EXTRACTOR_common_unzip_go_find_local_file (uf, name, 2)) 933 return; 934 if (EXTRACTOR_UNZIP_OK != 935 EXTRACTOR_common_unzip_open_current_file (uf)) 936 return; 937 /* One byte of slack so that the window is always 0-terminated: the 938 attribute values we hand to xml_unescape() point into it. */ 939 if (NULL == (buf = malloc (SCAN_CHUNK + 1))) 940 { 941 EXTRACTOR_common_unzip_close_current_file (uf); 942 return; 943 } 944 while (0 == mc->stop) 945 { 946 ssize_t got; 947 size_t have; 948 size_t used; 949 950 got = EXTRACTOR_common_unzip_read_current_file (uf, 951 &buf[carry], 952 SCAN_CHUNK - carry); 953 if (0 >= got) 954 break; 955 have = carry + (size_t) got; 956 total += (uint64_t) got; 957 buf[have] = '\0'; 958 used = scan_tags (mc, buf, have); 959 carry = have - used; 960 if (MAX_TAG < carry) 961 carry = 0; /* absurdly long tag; resynchronise */ 962 if (0 != carry) 963 memmove (buf, &buf[used], carry); 964 if (MAX_SCAN_BYTES < total) 965 break; 966 } 967 free (buf); 968 EXTRACTOR_common_unzip_close_current_file (uf); 969 } 970 971 972 /** 973 * Does @a name start with @a prefix? 974 * 975 * @param name string to test 976 * @param prefix prefix to test for 977 * @return 1 on match, 0 otherwise 978 */ 979 static int 980 has_prefix (const char *name, 981 const char *prefix) 982 { 983 return 0 == strncmp (name, prefix, strlen (prefix)); 984 } 985 986 987 /** 988 * Does @a name end with @a suffix? 989 * 990 * @param name string to test 991 * @param suffix suffix to test for 992 * @return 1 on match, 0 otherwise 993 */ 994 static int 995 has_suffix (const char *name, 996 const char *suffix) 997 { 998 size_t nl = strlen (name); 999 size_t sl = strlen (suffix); 1000 1001 return (nl >= sl) && (0 == strcmp (&name[nl - sl], suffix)); 1002 } 1003 1004 1005 /** 1006 * Is @a name a part that can name the people who worked on the 1007 * document? 1008 * 1009 * @param name name of a part inside the OOXML package 1010 * @return 1 if the part is worth scanning, 0 otherwise 1011 */ 1012 static int 1013 is_people_part (const char *name) 1014 { 1015 if (! has_suffix (name, ".xml")) 1016 return 0; 1017 /* Word: tracked changes live in the document body and in every 1018 story around it; comments and the identities behind them live in 1019 parts of their own. */ 1020 if ( (0 == strcmp (name, "word/document.xml")) || 1021 (0 == strcmp (name, "word/footnotes.xml")) || 1022 (0 == strcmp (name, "word/endnotes.xml")) || 1023 (0 == strcmp (name, "word/comments.xml")) || 1024 (0 == strcmp (name, "word/commentsExtended.xml")) || 1025 (0 == strcmp (name, "word/people.xml")) || 1026 (has_prefix (name, "word/header")) || 1027 (has_prefix (name, "word/footer")) ) 1028 return 1; 1029 /* Excel: the workbook part carries <fileSharing userName="...">, 1030 the direct descendant of the BIFF FILESHARING record. */ 1031 if (0 == strcmp (name, "xl/workbook.xml")) 1032 return 1; 1033 /* Excel: the shared workbook revision log, the modern threaded 1034 comment authors and the classic comment author list. */ 1035 if ( (has_prefix (name, "xl/revisions/")) || 1036 (has_prefix (name, "xl/persons/")) || 1037 (has_prefix (name, "xl/threadedComments/")) || 1038 (has_prefix (name, "xl/comments")) ) 1039 return 1; 1040 /* PowerPoint: the comment author list and the comments themselves. */ 1041 if ( (0 == strcmp (name, "ppt/commentAuthors.xml")) || 1042 (0 == strcmp (name, "ppt/authors.xml")) || 1043 (has_prefix (name, "ppt/comments/")) ) 1044 return 1; 1045 return 0; 1046 } 1047 1048 1049 /* ******************** the two formats ******************** */ 1050 1051 1052 /** 1053 * Extract meta data from an Office Open XML package. 1054 * 1055 * @param mc our extraction state 1056 * @return 1 if the file was an OOXML package, 0 if not 1057 */ 1058 static int 1059 extract_ooxml (struct MsoContext *mc) 1060 { 1061 struct EXTRACTOR_UnzipFile *uf; 1062 char *parts[MAX_PARTS]; 1063 unsigned int num_parts = 0; 1064 unsigned int i; 1065 const char *mime = NULL; 1066 char *buf; 1067 int ret; 1068 1069 if (NULL == (uf = EXTRACTOR_common_unzip_open (mc->ec))) 1070 return 0; 1071 /* First pass: learn what kind of document this is and which parts 1072 are worth looking at. We collect the names instead of extracting 1073 as we iterate, so that seeking around the archive cannot disturb 1074 the walk over the central directory. */ 1075 ret = EXTRACTOR_common_unzip_go_to_first_file (uf); 1076 while (EXTRACTOR_UNZIP_OK == ret) 1077 { 1078 char name[MAXFILENAME]; 1079 1080 if (EXTRACTOR_UNZIP_OK == 1081 EXTRACTOR_common_unzip_get_current_file_info (uf, NULL, 1082 name, sizeof (name), 1083 NULL, 0, NULL, 0)) 1084 { 1085 name[sizeof (name) - 1] = '\0'; 1086 if (0 == strcmp (name, "word/document.xml")) 1087 mime = 1088 "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; 1089 else if (0 == strcmp (name, "xl/workbook.xml")) 1090 mime = 1091 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; 1092 else if (0 == strcmp (name, "ppt/presentation.xml")) 1093 mime = 1094 "application/vnd.openxmlformats-officedocument.presentationml.presentation"; 1095 if ( (is_people_part (name)) && 1096 (MAX_PARTS > num_parts) ) 1097 { 1098 char *dup = strdup (name); 1099 1100 if (NULL != dup) 1101 parts[num_parts++] = dup; 1102 } 1103 } 1104 ret = EXTRACTOR_common_unzip_go_to_next_file (uf); 1105 } 1106 if (NULL == mime) 1107 { 1108 /* Not an OOXML document -- most likely an ODF file or a plain ZIP, 1109 both of which have plugins of their own. */ 1110 for (i = 0; i < num_parts; i++) 1111 free (parts[i]); 1112 EXTRACTOR_common_unzip_close (uf); 1113 return 0; 1114 } 1115 add_meta (mc, EXTRACTOR_METATYPE_MIMETYPE, mime); 1116 1117 if (NULL != (buf = read_part (uf, "docProps/core.xml"))) 1118 { 1119 for (i = 0; NULL != core_map[i].text; i++) 1120 report_element (mc, buf, core_map[i].text, core_map[i].type); 1121 free (buf); 1122 } 1123 if (NULL != (buf = read_part (uf, "docProps/app.xml"))) 1124 { 1125 for (i = 0; NULL != app_map[i].text; i++) 1126 report_element (mc, buf, app_map[i].text, app_map[i].type); 1127 free (buf); 1128 } 1129 if (NULL != (buf = read_part (uf, "docProps/custom.xml"))) 1130 { 1131 for (i = 0; NULL != custom_map[i].text; i++) 1132 report_custom_property (mc, buf, custom_map[i].text, custom_map[i].type); 1133 free (buf); 1134 } 1135 for (i = 0; (i < num_parts) && (0 == mc->stop); i++) 1136 { 1137 /* Excel keeps the authors of classic cell comments in a list of 1138 <author> elements rather than in an attribute. */ 1139 if (has_prefix (parts[i], "xl/comments")) 1140 { 1141 if (NULL != (buf = read_part (uf, parts[i]))) 1142 { 1143 report_element (mc, buf, "author", 1144 EXTRACTOR_METATYPE_CONTRIBUTOR_NAME); 1145 free (buf); 1146 } 1147 } 1148 scan_part (mc, uf, parts[i]); 1149 } 1150 for (i = 0; i < num_parts; i++) 1151 free (parts[i]); 1152 EXTRACTOR_common_unzip_close (uf); 1153 return 1; 1154 } 1155 1156 1157 /** 1158 * Extract meta data from a bare BIFF stream, as written by Excel 2 to 1159 * Excel 4 (and still produced by some exporters). 1160 * 1161 * @param mc our extraction state 1162 * @return 1 if the file was a BIFF stream, 0 if not 1163 */ 1164 static int 1165 extract_biff (struct MsoContext *mc) 1166 { 1167 struct EXTRACTOR_ExtractContext *ec = mc->ec; 1168 void *data; 1169 ssize_t avail; 1170 uint64_t fsize; 1171 1172 fsize = ec->get_size (ec->cls); 1173 if ( (8 > fsize) || 1174 (0 != ec->seek (ec->cls, 0, SEEK_SET)) ) 1175 return 0; 1176 /* The File Protection Block is the very first thing in the stream, 1177 so a small window is enough. */ 1178 avail = ec->read (ec->cls, 1179 &data, 1180 (fsize < 64 * 1024) ? (size_t) fsize : 64 * 1024); 1181 if (8 > avail) 1182 return 0; 1183 if (0 > EXTRACTOR_msoffice_biff_extract ((const unsigned char *) data, 1184 (size_t) avail, 1185 PLUGIN_NAME, 1186 ec->proc, 1187 ec->cls)) 1188 return 0; 1189 add_meta (mc, EXTRACTOR_METATYPE_MIMETYPE, "application/vnd.ms-excel"); 1190 return 1; 1191 } 1192 1193 1194 /** 1195 * Main entry method for the MS Office extraction plugin. 1196 * 1197 * @param ec extraction context provided to the plugin 1198 */ 1199 void 1200 EXTRACTOR_msoffice_extract_method (struct EXTRACTOR_ExtractContext *ec); 1201 1202 1203 void 1204 EXTRACTOR_msoffice_extract_method (struct EXTRACTOR_ExtractContext *ec) 1205 { 1206 struct MsoContext mc; 1207 unsigned int i; 1208 void *data; 1209 ssize_t avail; 1210 1211 memset (&mc, 0, sizeof (mc)); 1212 mc.ec = ec; 1213 if (0 != ec->seek (ec->cls, 0, SEEK_SET)) 1214 return; 1215 if (4 > (avail = ec->read (ec->cls, &data, 4))) 1216 return; 1217 if (0 == memcmp (data, "PK\003\004", 4)) 1218 extract_ooxml (&mc); 1219 else 1220 extract_biff (&mc); 1221 for (i = 0; i < mc.seen_len; i++) 1222 free (mc.seen[i]); 1223 } 1224 1225 1226 /* end of msoffice_extractor.c */