libextractor

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

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