(root)/
glib-2.79.0/
gio/
thumbnail-verify.c
       1  /* Copyright © 2013 Canonical Limited
       2   *
       3   * SPDX-License-Identifier: LGPL-2.1-or-later
       4   *
       5   * This library is free software; you can redistribute it and/or
       6   * modify it under the terms of the GNU Lesser General Public
       7   * License as published by the Free Software Foundation; either
       8   * version 2.1 of the License, or (at your option) any later version.
       9   *
      10   * This library is distributed in the hope that it will be useful,
      11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
      12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
      13   * Lesser General Public License for more details.
      14   *
      15   * You should have received a copy of the GNU Lesser General
      16   * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
      17   *
      18   * Author: Ryan Lortie <desrt@desrt.ca>
      19   */
      20  
      21  #include "config.h"
      22  
      23  #include "thumbnail-verify.h"
      24  
      25  #include <string.h>
      26  
      27  /* Begin code to check the validity of thumbnail files.  In order to do
      28   * that we need to parse enough PNG in order to get the Thumb::URI,
      29   * Thumb::MTime and Thumb::Size tags out of the file.  Fortunately this
      30   * is relatively easy.
      31   */
      32  typedef struct
      33  {
      34    const gchar *uri;
      35    guint64      mtime;
      36    guint64      size;
      37  } ExpectedInfo;
      38  
      39  /* We *require* matches on URI and MTime, but the Size field is optional
      40   * (as per the spec).
      41   *
      42   * http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
      43   */
      44  #define MATCHED_URI    (1u << 0)
      45  #define MATCHED_MTIME  (1u << 1)
      46  #define MATCHED_ALL    (MATCHED_URI | MATCHED_MTIME)
      47  
      48  static gboolean
      49  check_integer_match (guint64      expected,
      50                       const gchar *value,
      51                       guint32      value_size)
      52  {
      53    /* Would be nice to g_ascii_strtoll here, but we don't have a variant
      54     * that works on strings that are not nul-terminated.
      55     *
      56     * It's easy enough to do it ourselves...
      57     */
      58    if (expected == 0)  /* special case: "0" */
      59      return value_size == 1 && value[0] == '0';
      60  
      61    /* Check each digit, as long as we have data from both */
      62    while (expected && value_size)
      63      {
      64        /* Check the low-order digit */
      65        if (value[value_size - 1] != (gchar) ((expected % 10) + '0'))
      66          return FALSE;
      67  
      68        /* Move on... */
      69        expected /= 10;
      70        value_size--;
      71      }
      72  
      73    /* Make sure nothing is left over, on either side */
      74    return !expected && !value_size;
      75  }
      76  
      77  static gboolean
      78  check_png_info_chunk (ExpectedInfo *expected_info,
      79                        const gchar  *key,
      80                        guint32       key_size,
      81                        const gchar  *value,
      82                        guint32       value_size,
      83                        guint        *required_matches)
      84  {
      85    if (key_size == 10 && memcmp (key, "Thumb::URI", 10) == 0)
      86      {
      87        gsize expected_size;
      88  
      89        expected_size = strlen (expected_info->uri);
      90  
      91        if (expected_size != value_size)
      92          return FALSE;
      93  
      94        if (memcmp (expected_info->uri, value, value_size) != 0)
      95          return FALSE;
      96  
      97        *required_matches |= MATCHED_URI;
      98      }
      99  
     100    else if (key_size == 12 && memcmp (key, "Thumb::MTime", 12) == 0)
     101      {
     102        if (!check_integer_match (expected_info->mtime, value, value_size))
     103          return FALSE;
     104  
     105        *required_matches |= MATCHED_MTIME;
     106      }
     107  
     108    else if (key_size == 11 && memcmp (key, "Thumb::Size", 11) == 0)
     109      {
     110        /* A match on Thumb::Size is not required for success, but if we
     111         * find this optional field and it's wrong, we should reject the
     112         * thumbnail.
     113         */
     114        if (!check_integer_match (expected_info->size, value, value_size))
     115          return FALSE;
     116      }
     117  
     118    return TRUE;
     119  }
     120  
     121  static gboolean
     122  check_thumbnail_validity (ExpectedInfo *expected_info,
     123                            const gchar  *contents,
     124                            gsize         size)
     125  {
     126    guint required_matches = 0;
     127  
     128    /* Reference: http://www.w3.org/TR/PNG/ */
     129    if (size < 8)
     130      return FALSE;
     131  
     132    if (memcmp (contents, "\x89PNG\r\n\x1a\n", 8) != 0)
     133      return FALSE;
     134  
     135    contents += 8, size -= 8;
     136  
     137    /* We need at least 12 bytes to have a chunk... */
     138    while (size >= 12)
     139      {
     140        guint32 chunk_size_be;
     141        guint32 chunk_size;
     142  
     143        /* PNG is not an aligned file format so we have to be careful
     144         * about reading integers...
     145         */
     146        memcpy (&chunk_size_be, contents, 4);
     147        chunk_size = GUINT32_FROM_BE (chunk_size_be);
     148  
     149        contents += 4, size -= 4;
     150  
     151        /* After consuming the size field, we need to have enough bytes
     152         * for 4 bytes type field, chunk_size bytes for data, then 4 byte
     153         * for CRC (which we ignore)
     154         *
     155         * We just read chunk_size from the file, so it may be very large.
     156         * Make sure it won't wrap when we add 8 to it.
     157         */
     158        if (G_MAXUINT32 - chunk_size < 8 || size < chunk_size + 8)
     159          goto out;
     160  
     161        /* We are only interested in tEXt fields */
     162        if (memcmp (contents, "tEXt", 4) == 0)
     163          {
     164            const gchar *key = contents + 4;
     165            guint32 key_size;
     166  
     167            /* We need to find the nul separator character that splits the
     168             * key/value.  The value is not terminated.
     169             *
     170             * If we find no nul then we just ignore the field.
     171             *
     172             * value may contain extra nuls, but check_png_info_chunk()
     173             * can handle that.
     174             */
     175            for (key_size = 0; key_size < chunk_size; key_size++)
     176              {
     177                if (key[key_size] == '\0')
     178                  {
     179                    const gchar *value;
     180                    guint32 value_size;
     181  
     182                    /* Since key_size < chunk_size, value_size is
     183                     * definitely non-negative.
     184                     */
     185                    value_size = chunk_size - key_size - 1;
     186                    value = key + key_size + 1;
     187  
     188                    /* We found the separator character. */
     189                    if (!check_png_info_chunk (expected_info,
     190                                               key, key_size,
     191                                               value, value_size,
     192                                               &required_matches))
     193                      return FALSE;
     194                  }
     195              }
     196          }
     197        else
     198          {
     199            /* A bit of a hack: assume that all tEXt chunks will appear
     200             * together.  Therefore, if we have already seen both required
     201             * fields and then see a non-tEXt chunk then we can assume we
     202             * are done.
     203             *
     204             * The common case is that the tEXt chunks come at the start
     205             * of the file before any of the image data.  This trick means
     206             * that we will only fault in a single page (4k) whereas many
     207             * thumbnails (particularly the large ones) can approach 100k
     208             * in size.
     209             */
     210            if (required_matches == MATCHED_ALL)
     211              goto out;
     212          }
     213  
     214        /* skip to the next chunk, ignoring CRC. */
     215        contents += 4, size -= 4;                         /* type field */
     216        contents += chunk_size, size -= chunk_size;       /* data */
     217        contents += 4, size -= 4;                         /* CRC */
     218      }
     219  
     220  out:
     221    return required_matches == MATCHED_ALL;
     222  }
     223  
     224  gboolean
     225  thumbnail_verify (const char     *thumbnail_path,
     226                    const gchar    *file_uri,
     227                    const GLocalFileStat *file_stat_buf)
     228  {
     229    gboolean thumbnail_is_valid = FALSE;
     230    ExpectedInfo expected_info;
     231    GMappedFile *file;
     232  
     233    if (file_stat_buf == NULL)
     234      return FALSE;
     235  
     236    expected_info.uri = file_uri;
     237  #ifdef G_OS_WIN32
     238    expected_info.mtime = (guint64) file_stat_buf->st_mtim.tv_sec;
     239  #else
     240    expected_info.mtime = _g_stat_mtime (file_stat_buf);
     241  #endif
     242    expected_info.size = _g_stat_size (file_stat_buf);
     243  
     244    file = g_mapped_file_new (thumbnail_path, FALSE, NULL);
     245    if (file)
     246      {
     247        thumbnail_is_valid = check_thumbnail_validity (&expected_info,
     248                                                       g_mapped_file_get_contents (file),
     249                                                       g_mapped_file_get_length (file));
     250        g_mapped_file_unref (file);
     251      }
     252  
     253    return thumbnail_is_valid;
     254  }