webp_extractor.c (19296B)
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/webp_extractor.c 22 * @brief plugin to support WebP images 23 * @author Christian Grothoff 24 * 25 * WebP is a RIFF container. We read the chunk headers and the few 26 * bytes of each bitstream header that carry the canvas size; no VP8 or 27 * VP8L data is decoded and libwebp is not linked, because this is a 28 * fast identification pass. 29 * 30 * Layout, per the WebP container specification: 31 * 32 * "RIFF" <uint32le size> "WEBP" 33 * then chunks of <FourCC> <uint32le size> <payload> [pad byte] 34 * 35 * A simple file has exactly one chunk, "VP8 " (lossy) or "VP8L" 36 * (lossless). An extended file starts with "VP8X", whose feature flags 37 * say what else to expect: "ICCP", "ALPH", "ANIM"/"ANMF", "EXIF" and 38 * "XMP ". 39 * 40 * The AVI plugin ("riff") also opens RIFF files, so this one insists on 41 * the "WEBP" form type; the two never claim the same file. 42 */ 43 #include "platform.h" 44 #include "extractor.h" 45 #include "forensics.h" 46 47 48 /** 49 * Maximum number of top level chunks we look at. 50 */ 51 #define WEBP_MAX_CHUNKS 1024 52 53 /** 54 * Maximum number of animation frames we walk. The frame count we 55 * report is the number we actually saw, so this caps the reported 56 * value too, which is what a characterisation pass wants. 57 */ 58 #define WEBP_MAX_FRAMES 512 59 60 /** 61 * Total number of bytes we are willing to read from one file. 62 */ 63 #define WEBP_READ_BUDGET (128 * 1024) 64 65 /* VP8X feature flags, in the single flag byte at the start of the 66 "VP8X" payload. Bit 0 (0x80) and bit 1 (0x40) are reserved. */ 67 #define WEBP_FLAG_ICC 0x20 68 #define WEBP_FLAG_ALPHA 0x10 69 #define WEBP_FLAG_EXIF 0x08 70 #define WEBP_FLAG_XMP 0x04 71 #define WEBP_FLAG_ANIMATION 0x02 72 73 74 /** 75 * Everything we learn while walking the chunks. 76 */ 77 struct WebpState 78 { 79 /** 80 * Extraction context we were handed. 81 */ 82 struct EXTRACTOR_ExtractContext *ec; 83 84 /** 85 * Bytes we may still read. 86 */ 87 uint64_t budget; 88 89 /** 90 * Canvas width in pixels, 0 if unknown. 91 */ 92 uint32_t width; 93 94 /** 95 * Canvas height in pixels, 0 if unknown. 96 */ 97 uint32_t height; 98 99 /** 100 * Size of the ICC profile in bytes, 0 if there is none. 101 */ 102 uint64_t icc_size; 103 104 /** 105 * Sum of the frame durations in milliseconds. 106 */ 107 uint64_t duration; 108 109 /** 110 * Number of "ANMF" frames seen. 111 */ 112 unsigned int frames; 113 114 /** 115 * Loop count from "ANIM", 0 meaning "forever". 116 */ 117 unsigned int loop_count; 118 119 /** 120 * Background colour from "ANIM", as BGRA. 121 */ 122 uint32_t background; 123 124 /** 125 * Feature flags from "VP8X". 126 */ 127 unsigned char flags; 128 129 /** 130 * True if a "VP8X" chunk was seen. 131 */ 132 int extended; 133 134 /** 135 * True if an "ANIM" chunk was seen. 136 */ 137 int animated; 138 139 /** 140 * True if lossy VP8 data was seen. 141 */ 142 int lossy; 143 144 /** 145 * True if lossless VP8L data was seen. 146 */ 147 int lossless; 148 149 /** 150 * True if an "ALPH" chunk or a VP8L alpha flag was seen. 151 */ 152 int alpha; 153 154 /** 155 * True if an "EXIF" chunk was seen. 156 */ 157 int exif; 158 159 /** 160 * True if an "XMP " chunk was seen. 161 */ 162 int xmp; 163 }; 164 165 166 /** 167 * Read @a len bytes at @a offset, charged against the read budget. 168 * 169 * @param ws parser state 170 * @param offset absolute offset to read from 171 * @param buf where to put the data 172 * @param len number of bytes to read 173 * @return 1 on success, 0 on a short file or an exhausted budget 174 */ 175 static int 176 webp_read (struct WebpState *ws, 177 uint64_t offset, 178 void *buf, 179 size_t len) 180 { 181 if ( (len > ws->budget) || 182 (offset > INT64_MAX) ) 183 return 0; 184 ws->budget -= len; 185 return EXTRACTOR_forensic_read_ (ws->ec, 186 (int64_t) offset, 187 buf, 188 len); 189 } 190 191 192 /** 193 * Read a 24-bit little-endian value. 194 * 195 * @param p three bytes 196 * @return the value 197 */ 198 static uint32_t 199 webp_le24 (const unsigned char *p) 200 { 201 return ((uint32_t) p[0]) 202 | (((uint32_t) p[1]) << 8) 203 | (((uint32_t) p[2]) << 16); 204 } 205 206 207 /** 208 * Pull the canvas size out of a lossy "VP8 " key frame header. 209 * 210 * The payload starts with a three byte frame tag, then the three byte 211 * start code 0x9d 0x01 0x2a, then two 16-bit little-endian values whose 212 * low 14 bits are the width and the height; the top two bits of each 213 * are an upscaling hint that does not change the coded size. 214 * 215 * @param ws parser state 216 * @param payload absolute offset of the chunk payload 217 * @param len length of the chunk payload 218 */ 219 static void 220 webp_parse_vp8 (struct WebpState *ws, 221 uint64_t payload, 222 uint64_t len) 223 { 224 unsigned char buf[10]; 225 226 ws->lossy = 1; 227 if (len < 10) 228 return; 229 if (! webp_read (ws, 230 payload, 231 buf, 232 sizeof (buf))) 233 return; 234 if (0 != (buf[0] & 0x01)) 235 return; /* not a key frame; no size here */ 236 if ( (0x9d != buf[3]) || 237 (0x01 != buf[4]) || 238 (0x2a != buf[5]) ) 239 return; /* start code missing */ 240 ws->width = EXTRACTOR_forensic_le16_ (&buf[6]) & 0x3FFF; 241 ws->height = EXTRACTOR_forensic_le16_ (&buf[8]) & 0x3FFF; 242 } 243 244 245 /** 246 * Pull the canvas size out of a lossless "VP8L" header. 247 * 248 * The payload starts with the signature byte 0x2f followed by a 32-bit 249 * little-endian word packing (width - 1) in bits 0..13, (height - 1) in 250 * bits 14..27, an alpha hint in bit 28 and the version in bits 29..31. 251 * 252 * @param ws parser state 253 * @param payload absolute offset of the chunk payload 254 * @param len length of the chunk payload 255 */ 256 static void 257 webp_parse_vp8l (struct WebpState *ws, 258 uint64_t payload, 259 uint64_t len) 260 { 261 unsigned char buf[5]; 262 uint32_t bits; 263 264 ws->lossless = 1; 265 if (len < 5) 266 return; 267 if (! webp_read (ws, 268 payload, 269 buf, 270 sizeof (buf))) 271 return; 272 if (0x2f != buf[0]) 273 return; /* signature missing */ 274 bits = EXTRACTOR_forensic_le32_ (&buf[1]); 275 ws->width = (bits & 0x3FFF) + 1; 276 ws->height = ((bits >> 14) & 0x3FFF) + 1; 277 if (0 != ((bits >> 28) & 0x01)) 278 ws->alpha = 1; 279 } 280 281 282 /** 283 * Parse the "VP8X" extended header: the feature flags and the canvas 284 * size, which are stored as 24-bit values one less than the real size. 285 * 286 * @param ws parser state 287 * @param payload absolute offset of the chunk payload 288 * @param len length of the chunk payload 289 */ 290 static void 291 webp_parse_vp8x (struct WebpState *ws, 292 uint64_t payload, 293 uint64_t len) 294 { 295 unsigned char buf[10]; 296 297 ws->extended = 1; 298 if (len < 10) 299 return; 300 if (! webp_read (ws, 301 payload, 302 buf, 303 sizeof (buf))) 304 return; 305 ws->flags = buf[0]; 306 ws->width = webp_le24 (&buf[4]) + 1; 307 ws->height = webp_le24 (&buf[7]) + 1; 308 if (0 != (ws->flags & WEBP_FLAG_ALPHA)) 309 ws->alpha = 1; 310 } 311 312 313 /** 314 * Parse an "ANIM" global animation header. 315 * 316 * @param ws parser state 317 * @param payload absolute offset of the chunk payload 318 * @param len length of the chunk payload 319 */ 320 static void 321 webp_parse_anim (struct WebpState *ws, 322 uint64_t payload, 323 uint64_t len) 324 { 325 unsigned char buf[6]; 326 327 ws->animated = 1; 328 if (len < 6) 329 return; 330 if (! webp_read (ws, 331 payload, 332 buf, 333 sizeof (buf))) 334 return; 335 ws->background = EXTRACTOR_forensic_le32_ (buf); 336 ws->loop_count = EXTRACTOR_forensic_le16_ (&buf[4]); 337 } 338 339 340 /** 341 * Parse an "ANMF" frame header, and look inside the frame for the 342 * sub-chunk that says how it is coded. The frame duration is a 24-bit 343 * value in milliseconds. 344 * 345 * @param ws parser state 346 * @param payload absolute offset of the chunk payload 347 * @param len length of the chunk payload 348 */ 349 static void 350 webp_parse_anmf (struct WebpState *ws, 351 uint64_t payload, 352 uint64_t len) 353 { 354 unsigned char buf[16]; 355 356 if (ws->frames >= WEBP_MAX_FRAMES) 357 return; 358 ws->frames++; 359 if (len < 16) 360 return; 361 if (! webp_read (ws, 362 payload, 363 buf, 364 sizeof (buf))) 365 return; 366 ws->duration += webp_le24 (&buf[12]); 367 /* The frame payload holds an optional "ALPH" chunk and then one 368 "VP8 " or "VP8L" chunk; peek at the first one only, so that an 369 extended file still reports a codec. */ 370 if ( (0 != ws->lossy) || 371 (0 != ws->lossless) || 372 (len < 24) ) 373 return; 374 if (! webp_read (ws, 375 payload + 16, 376 buf, 377 8)) 378 return; 379 if (0 == memcmp (buf, 380 "ALPH", 381 4)) 382 { 383 ws->alpha = 1; 384 return; 385 } 386 if (0 == memcmp (buf, 387 "VP8 ", 388 4)) 389 ws->lossy = 1; 390 else if (0 == memcmp (buf, 391 "VP8L", 392 4)) 393 ws->lossless = 1; 394 } 395 396 397 /** 398 * Append @a name to the comma separated list in @a buf, if @a present. 399 * 400 * @param buf the list being built, always NUL terminated 401 * @param size number of bytes in @a buf 402 * @param[in,out] off current length of the list 403 * @param present whether the feature is there at all 404 * @param name what to append 405 */ 406 static void 407 webp_append (char *buf, 408 size_t size, 409 size_t *off, 410 int present, 411 const char *name) 412 { 413 size_t need; 414 415 if (! present) 416 return; 417 need = strlen (name) + ((0 != *off) ? 2 : 0); 418 if (*off + need + 1 > size) 419 return; 420 if (0 != *off) 421 { 422 buf[(*off)++] = ','; 423 buf[(*off)++] = ' '; 424 } 425 memcpy (&buf[*off], 426 name, 427 strlen (name)); 428 *off += strlen (name); 429 buf[*off] = '\0'; 430 } 431 432 433 /** 434 * Main entry method for the 'image/webp' extraction plugin. 435 * 436 * @param ec extraction context provided to the plugin 437 */ 438 void 439 EXTRACTOR_webp_extract_method (struct EXTRACTOR_ExtractContext *ec); 440 441 void 442 EXTRACTOR_webp_extract_method (struct EXTRACTOR_ExtractContext *ec) 443 { 444 struct WebpState ws; 445 unsigned char head[12]; 446 uint64_t fsize; 447 uint64_t riff_end; 448 uint64_t pos; 449 uint32_t riff_size; 450 char attrs[256]; 451 size_t aoff = 0; 452 453 fsize = ec->get_size (ec->cls); 454 if ( (UINT64_MAX == fsize) || 455 (fsize < 12) ) 456 return; 457 memset (&ws, 458 0, 459 sizeof (ws)); 460 ws.ec = ec; 461 ws.budget = WEBP_READ_BUDGET; 462 463 /* Twelve bytes decide it; a file that is not ours costs nothing more 464 than this one read. */ 465 if (! webp_read (&ws, 466 0, 467 head, 468 sizeof (head))) 469 return; 470 if ( (0 != memcmp (head, 471 "RIFF", 472 4)) || 473 (0 != memcmp (&head[8], 474 "WEBP", 475 4)) ) 476 return; 477 riff_size = EXTRACTOR_forensic_le32_ (&head[4]); 478 /* The RIFF size counts everything after the eight byte RIFF header; 479 trust the file only as far as its actual length. */ 480 riff_end = (uint64_t) riff_size + 8; 481 if (riff_end > fsize) 482 riff_end = fsize; 483 484 pos = 12; 485 for (unsigned int n = 0; 486 (n < WEBP_MAX_CHUNKS) && (pos + 8 <= riff_end); 487 n++) 488 { 489 unsigned char chdr[8]; 490 uint64_t csize; 491 uint64_t payload; 492 uint64_t advance; 493 494 if (! webp_read (&ws, 495 pos, 496 chdr, 497 sizeof (chdr))) 498 break; 499 csize = EXTRACTOR_forensic_le32_ (&chdr[4]); 500 payload = pos + 8; 501 if (csize > riff_end - payload) 502 csize = riff_end - payload; /* truncated file; use what is there */ 503 if (0 == memcmp (chdr, 504 "VP8X", 505 4)) 506 webp_parse_vp8x (&ws, 507 payload, 508 csize); 509 else if (0 == memcmp (chdr, 510 "VP8 ", 511 4)) 512 webp_parse_vp8 (&ws, 513 payload, 514 csize); 515 else if (0 == memcmp (chdr, 516 "VP8L", 517 4)) 518 webp_parse_vp8l (&ws, 519 payload, 520 csize); 521 else if (0 == memcmp (chdr, 522 "ANIM", 523 4)) 524 webp_parse_anim (&ws, 525 payload, 526 csize); 527 else if (0 == memcmp (chdr, 528 "ANMF", 529 4)) 530 webp_parse_anmf (&ws, 531 payload, 532 csize); 533 else if (0 == memcmp (chdr, 534 "ALPH", 535 4)) 536 ws.alpha = 1; 537 else if (0 == memcmp (chdr, 538 "ICCP", 539 4)) 540 ws.icc_size = csize; 541 else if (0 == memcmp (chdr, 542 "EXIF", 543 4)) 544 ws.exif = 1; 545 else if (0 == memcmp (chdr, 546 "XMP ", 547 4)) 548 ws.xmp = 1; 549 /* Chunks are padded to an even length. The eight byte header 550 guarantees forward progress even for a zero length chunk. */ 551 advance = 8 + csize + (csize & 1); 552 if (advance < 8) 553 break; 554 pos += advance; 555 } 556 557 if (0 != ec->proc (ec->cls, 558 "webp", 559 EXTRACTOR_METATYPE_MIMETYPE, 560 EXTRACTOR_METAFORMAT_UTF8, 561 "text/plain", 562 "image/webp", 563 strlen ("image/webp") + 1)) 564 return; 565 if (0 != EXTRACTOR_forensic_emit_ (ec, 566 "webp", 567 EXTRACTOR_METATYPE_FORMAT, 568 "WebP (%s)", 569 ws.extended 570 ? "extended" 571 : (ws.lossless ? "lossless" : "lossy"))) 572 return; 573 if ( (ws.lossy) || 574 (ws.lossless) ) 575 { 576 if (0 != EXTRACTOR_forensic_emit_ (ec, 577 "webp", 578 EXTRACTOR_METATYPE_CODEC, 579 "%s", 580 ws.lossless 581 ? "VP8L (lossless)" 582 : "VP8 (lossy)")) 583 return; 584 } 585 if ( (0 != ws.width) && 586 (0 != ws.height) ) 587 { 588 if (0 != EXTRACTOR_forensic_emit_ (ec, 589 "webp", 590 EXTRACTOR_METATYPE_IMAGE_DIMENSIONS, 591 "%ux%u", 592 (unsigned int) ws.width, 593 (unsigned int) ws.height)) 594 return; 595 } 596 /* One ATTRIBUTES line naming the optional features, so that a caller 597 can filter on "has an alpha channel" or "is animated" without 598 having to reason about the flag byte. */ 599 attrs[0] = '\0'; 600 webp_append (attrs, 601 sizeof (attrs), 602 &aoff, 603 ws.alpha, 604 "alpha"); 605 webp_append (attrs, 606 sizeof (attrs), 607 &aoff, 608 ws.animated, 609 "animation"); 610 webp_append (attrs, 611 sizeof (attrs), 612 &aoff, 613 0 != ws.icc_size, 614 "ICC profile"); 615 webp_append (attrs, 616 sizeof (attrs), 617 &aoff, 618 ws.exif, 619 "EXIF"); 620 webp_append (attrs, 621 sizeof (attrs), 622 &aoff, 623 ws.xmp, 624 "XMP"); 625 if (0 != aoff) 626 { 627 if (0 != EXTRACTOR_forensic_emit_ (ec, 628 "webp", 629 EXTRACTOR_METATYPE_ATTRIBUTES, 630 "%s", 631 attrs)) 632 return; 633 } 634 /* WebP has no depth field of its own: both bitstreams are 8 bits per 635 channel by definition. Report the bare number so that a caller can 636 compare it with what the other image plugins report; whether there 637 is an alpha channel is an attribute, not a depth. */ 638 if (0 != EXTRACTOR_forensic_emit_ (ec, 639 "webp", 640 EXTRACTOR_METATYPE_COLOR_DEPTH, 641 "8")) 642 return; 643 if (0 != ws.icc_size) 644 { 645 /* Not parsed here on purpose: the ICC payload is the exiv2 and 646 colour management plugins' business. */ 647 if (0 != EXTRACTOR_forensic_emit_ (ec, 648 "webp", 649 EXTRACTOR_METATYPE_COLOR_PROFILE, 650 "ICC profile, %llu bytes", 651 (unsigned long long) ws.icc_size)) 652 return; 653 } 654 if (ws.animated) 655 { 656 if (0 != EXTRACTOR_forensic_emit_ (ec, 657 "webp", 658 EXTRACTOR_METATYPE_ENTRY_COUNT, 659 "%u", 660 ws.frames)) 661 return; 662 if (0 != EXTRACTOR_forensic_emit_ (ec, 663 "webp", 664 EXTRACTOR_METATYPE_DURATION, 665 "%llu ms", 666 (unsigned long long) ws.duration)) 667 return; 668 if (0 != EXTRACTOR_forensic_emit_ (ec, 669 "webp", 670 EXTRACTOR_METATYPE_COMMENT, 671 "animation: %u frame%s, %s, " 672 "background #%08x BGRA", 673 ws.frames, 674 (1 == ws.frames) ? "" : "s", 675 (0 == ws.loop_count) 676 ? "looping forever" 677 : "finite loop count", 678 (unsigned int) ws.background)) 679 return; 680 } 681 if (ws.exif) 682 { 683 if (0 != EXTRACTOR_forensic_emit_ (ec, 684 "webp", 685 EXTRACTOR_METATYPE_COMMENT, 686 "contains an EXIF chunk")) 687 return; 688 } 689 if (ws.xmp) 690 { 691 if (0 != EXTRACTOR_forensic_emit_ (ec, 692 "webp", 693 EXTRACTOR_METATYPE_COMMENT, 694 "contains an XMP chunk")) 695 return; 696 } 697 } 698 699 700 /* end of webp_extractor.c */