Coverage Report

Created: 2025-06-13 06:55

/src/glib/gio/thumbnail-verify.c
Line
Count
Source (jump to first uncovered line)
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
0
#define MATCHED_URI    (1u << 0)
45
0
#define MATCHED_MTIME  (1u << 1)
46
0
#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
0
{
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
0
  if (expected == 0)  /* special case: "0" */
59
0
    return value_size == 1 && value[0] == '0';
60
61
  /* Check each digit, as long as we have data from both */
62
0
  while (expected && value_size)
63
0
    {
64
      /* Check the low-order digit */
65
0
      if (value[value_size - 1] != (gchar) ((expected % 10) + '0'))
66
0
        return FALSE;
67
68
      /* Move on... */
69
0
      expected /= 10;
70
0
      value_size--;
71
0
    }
72
73
  /* Make sure nothing is left over, on either side */
74
0
  return !expected && !value_size;
75
0
}
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
0
{
85
0
  if (key_size == 10 && memcmp (key, "Thumb::URI", 10) == 0)
86
0
    {
87
0
      gsize expected_size;
88
89
0
      expected_size = strlen (expected_info->uri);
90
91
0
      if (expected_size != value_size)
92
0
        return FALSE;
93
94
0
      if (memcmp (expected_info->uri, value, value_size) != 0)
95
0
        return FALSE;
96
97
0
      *required_matches |= MATCHED_URI;
98
0
    }
99
100
0
  else if (key_size == 12 && memcmp (key, "Thumb::MTime", 12) == 0)
101
0
    {
102
0
      if (!check_integer_match (expected_info->mtime, value, value_size))
103
0
        return FALSE;
104
105
0
      *required_matches |= MATCHED_MTIME;
106
0
    }
107
108
0
  else if (key_size == 11 && memcmp (key, "Thumb::Size", 11) == 0)
109
0
    {
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
0
      if (!check_integer_match (expected_info->size, value, value_size))
115
0
        return FALSE;
116
0
    }
117
118
0
  return TRUE;
119
0
}
120
121
static gboolean
122
check_thumbnail_validity (ExpectedInfo *expected_info,
123
                          const gchar  *contents,
124
                          gsize         size)
125
0
{
126
0
  guint required_matches = 0;
127
128
  /* Reference: http://www.w3.org/TR/PNG/ */
129
0
  if (size < 8)
130
0
    return FALSE;
131
132
0
  if (memcmp (contents, "\x89PNG\r\n\x1a\n", 8) != 0)
133
0
    return FALSE;
134
135
0
  contents += 8, size -= 8;
136
137
  /* We need at least 12 bytes to have a chunk... */
138
0
  while (size >= 12)
139
0
    {
140
0
      guint32 chunk_size_be;
141
0
      guint32 chunk_size;
142
143
      /* PNG is not an aligned file format so we have to be careful
144
       * about reading integers...
145
       */
146
0
      memcpy (&chunk_size_be, contents, 4);
147
0
      chunk_size = GUINT32_FROM_BE (chunk_size_be);
148
149
0
      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
0
      if (G_MAXUINT32 - chunk_size < 8 || size < chunk_size + 8)
159
0
        goto out;
160
161
      /* We are only interested in tEXt fields */
162
0
      if (memcmp (contents, "tEXt", 4) == 0)
163
0
        {
164
0
          const gchar *key = contents + 4;
165
0
          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
0
          for (key_size = 0; key_size < chunk_size; key_size++)
176
0
            {
177
0
              if (key[key_size] == '\0')
178
0
                {
179
0
                  const gchar *value;
180
0
                  guint32 value_size;
181
182
                  /* Since key_size < chunk_size, value_size is
183
                   * definitely non-negative.
184
                   */
185
0
                  value_size = chunk_size - key_size - 1;
186
0
                  value = key + key_size + 1;
187
188
                  /* We found the separator character. */
189
0
                  if (!check_png_info_chunk (expected_info,
190
0
                                             key, key_size,
191
0
                                             value, value_size,
192
0
                                             &required_matches))
193
0
                    return FALSE;
194
0
                }
195
0
            }
196
0
        }
197
0
      else
198
0
        {
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
0
          if (required_matches == MATCHED_ALL)
211
0
            goto out;
212
0
        }
213
214
      /* skip to the next chunk, ignoring CRC. */
215
0
      contents += 4, size -= 4;                         /* type field */
216
0
      contents += chunk_size, size -= chunk_size;       /* data */
217
0
      contents += 4, size -= 4;                         /* CRC */
218
0
    }
219
220
0
out:
221
0
  return required_matches == MATCHED_ALL;
222
0
}
223
224
gboolean
225
thumbnail_verify (const char     *thumbnail_path,
226
                  const gchar    *file_uri,
227
                  const GLocalFileStat *file_stat_buf)
228
0
{
229
0
  gboolean thumbnail_is_valid = FALSE;
230
0
  ExpectedInfo expected_info;
231
0
  GMappedFile *file;
232
233
0
  if (file_stat_buf == NULL)
234
0
    return FALSE;
235
236
0
  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
0
  expected_info.mtime = _g_stat_mtime (file_stat_buf);
241
0
#endif
242
0
  expected_info.size = _g_stat_size (file_stat_buf);
243
244
0
  file = g_mapped_file_new (thumbnail_path, FALSE, NULL);
245
0
  if (file)
246
0
    {
247
0
      thumbnail_is_valid = check_thumbnail_validity (&expected_info,
248
0
                                                     g_mapped_file_get_contents (file),
249
0
                                                     g_mapped_file_get_length (file));
250
0
      g_mapped_file_unref (file);
251
0
    }
252
253
0
  return thumbnail_is_valid;
254
0
}