Update tests for new truncate behavior
[platform/upstream/glib.git] / gio / thumbnail-verify.c
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 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, write to the
15  * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
16  * Boston, MA 02111-1307, USA.
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 GStatBuf *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   expected_info.mtime = file_stat_buf->st_mtime;
238   expected_info.size = file_stat_buf->st_size;
239
240   file = g_mapped_file_new (thumbnail_path, FALSE, NULL);
241   if (file)
242     {
243       thumbnail_is_valid = check_thumbnail_validity (&expected_info,
244                                                      g_mapped_file_get_contents (file),
245                                                      g_mapped_file_get_length (file));
246       g_mapped_file_unref (file);
247     }
248
249   return thumbnail_is_valid;
250 }