Branch data Line data Source code
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 : 11 : 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 : 11 : if (expected == 0) /* special case: "0" */
59 : 1 : return value_size == 1 && value[0] == '0';
60 : :
61 : : /* Check each digit, as long as we have data from both */
62 : 64 : while (expected && value_size)
63 : : {
64 : : /* Check the low-order digit */
65 : 56 : if (value[value_size - 1] != (gchar) ((expected % 10) + '0'))
66 : 2 : return FALSE;
67 : :
68 : : /* Move on... */
69 : 54 : expected /= 10;
70 : 54 : value_size--;
71 : : }
72 : :
73 : : /* Make sure nothing is left over, on either side */
74 : 8 : return !expected && !value_size;
75 : : }
76 : :
77 : : static gboolean
78 : 324 : 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 : 324 : if (key_size == 10 && memcmp (key, "Thumb::URI", 10) == 0)
86 : 6 : {
87 : : gsize expected_size;
88 : :
89 : 101 : expected_size = strlen (expected_info->uri);
90 : :
91 : 101 : if (expected_size != value_size)
92 : 95 : return FALSE;
93 : :
94 : 6 : if (memcmp (expected_info->uri, value, value_size) != 0)
95 : 0 : return FALSE;
96 : :
97 : 6 : *required_matches |= MATCHED_URI;
98 : : }
99 : :
100 : 223 : else if (key_size == 12 && memcmp (key, "Thumb::MTime", 12) == 0)
101 : : {
102 : 6 : if (!check_integer_match (expected_info->mtime, value, value_size))
103 : 2 : return FALSE;
104 : :
105 : 4 : *required_matches |= MATCHED_MTIME;
106 : : }
107 : :
108 : 217 : 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 : 5 : if (!check_integer_match (expected_info->size, value, value_size))
115 : 1 : return FALSE;
116 : : }
117 : :
118 : 226 : return TRUE;
119 : : }
120 : :
121 : : static gboolean
122 : 107 : check_thumbnail_validity (ExpectedInfo *expected_info,
123 : : const gchar *contents,
124 : : gsize size)
125 : : {
126 : 107 : guint required_matches = 0;
127 : :
128 : : /* Reference: http://www.w3.org/TR/PNG/ */
129 : 107 : if (size < 8)
130 : 1 : return FALSE;
131 : :
132 : 106 : if (memcmp (contents, "\x89PNG\r\n\x1a\n", 8) != 0)
133 : 1 : return FALSE;
134 : :
135 : 105 : contents += 8, size -= 8;
136 : :
137 : : /* We need at least 12 bytes to have a chunk... */
138 : 539 : 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 : 539 : memcpy (&chunk_size_be, contents, 4);
147 : 539 : chunk_size = GUINT32_FROM_BE (chunk_size_be);
148 : :
149 : 539 : 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 : 539 : if (G_MAXUINT32 - chunk_size < 8 || size < chunk_size + 8)
159 : 7 : goto out;
160 : :
161 : : /* We are only interested in tEXt fields */
162 : 532 : if (memcmp (contents, "tEXt", 4) == 0)
163 : : {
164 : 323 : 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 : 6665 : for (key_size = 0; key_size < chunk_size; key_size++)
176 : : {
177 : 6440 : 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 : 324 : value_size = chunk_size - key_size - 1;
186 : 324 : value = key + key_size + 1;
187 : :
188 : : /* We found the separator character. */
189 : 324 : if (!check_png_info_chunk (expected_info,
190 : : key, key_size,
191 : : value, value_size,
192 : : &required_matches))
193 : 98 : 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 : 209 : if (required_matches == MATCHED_ALL)
211 : 0 : goto out;
212 : : }
213 : :
214 : : /* skip to the next chunk, ignoring CRC. */
215 : 434 : contents += 4, size -= 4; /* type field */
216 : 434 : contents += chunk_size, size -= chunk_size; /* data */
217 : 434 : contents += 4, size -= 4; /* CRC */
218 : : }
219 : :
220 : 0 : out:
221 : 7 : return required_matches == MATCHED_ALL;
222 : : }
223 : :
224 : : gboolean
225 : 108 : thumbnail_verify (const char *thumbnail_path,
226 : : const gchar *file_uri,
227 : : const GLocalFileStat *file_stat_buf)
228 : : {
229 : 108 : gboolean thumbnail_is_valid = FALSE;
230 : : ExpectedInfo expected_info;
231 : : GMappedFile *file;
232 : :
233 : 108 : if (file_stat_buf == NULL)
234 : 0 : return FALSE;
235 : :
236 : 108 : 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 : 108 : expected_info.mtime = _g_stat_mtime (file_stat_buf);
241 : : #endif
242 : 108 : expected_info.size = _g_stat_size (file_stat_buf);
243 : :
244 : 108 : file = g_mapped_file_new (thumbnail_path, FALSE, NULL);
245 : 108 : if (file)
246 : : {
247 : 214 : thumbnail_is_valid = check_thumbnail_validity (&expected_info,
248 : 107 : g_mapped_file_get_contents (file),
249 : : g_mapped_file_get_length (file));
250 : 107 : g_mapped_file_unref (file);
251 : : }
252 : :
253 : 108 : return thumbnail_is_valid;
254 : : }
|