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