| /* Copyright © 2013 Canonical Limited |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General |
| * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. |
| * |
| * Author: Ryan Lortie <desrt@desrt.ca> |
| */ |
| |
| #include "config.h" |
| |
| #include "thumbnail-verify.h" |
| |
| #include <string.h> |
| |
| /* Begin code to check the validity of thumbnail files. In order to do |
| * that we need to parse enough PNG in order to get the Thumb::URI, |
| * Thumb::MTime and Thumb::Size tags out of the file. Fortunately this |
| * is relatively easy. |
| */ |
| typedef struct |
| { |
| const gchar *uri; |
| guint64 mtime; |
| guint64 size; |
| } ExpectedInfo; |
| |
| /* We *require* matches on URI and MTime, but the Size field is optional |
| * (as per the spec). |
| * |
| * http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html |
| */ |
| #define MATCHED_URI (1u << 0) |
| #define MATCHED_MTIME (1u << 1) |
| #define MATCHED_ALL (MATCHED_URI | MATCHED_MTIME) |
| |
| static gboolean |
| check_integer_match (guint64 expected, |
| const gchar *value, |
| guint32 value_size) |
| { |
| /* Would be nice to g_ascii_strtoll here, but we don't have a variant |
| * that works on strings that are not nul-terminated. |
| * |
| * It's easy enough to do it ourselves... |
| */ |
| if (expected == 0) /* special case: "0" */ |
| return value_size == 1 && value[0] == '0'; |
| |
| /* Check each digit, as long as we have data from both */ |
| while (expected && value_size) |
| { |
| /* Check the low-order digit */ |
| if (value[value_size - 1] != (gchar) ((expected % 10) + '0')) |
| return FALSE; |
| |
| /* Move on... */ |
| expected /= 10; |
| value_size--; |
| } |
| |
| /* Make sure nothing is left over, on either side */ |
| return !expected && !value_size; |
| } |
| |
| static gboolean |
| check_png_info_chunk (ExpectedInfo *expected_info, |
| const gchar *key, |
| guint32 key_size, |
| const gchar *value, |
| guint32 value_size, |
| guint *required_matches) |
| { |
| if (key_size == 10 && memcmp (key, "Thumb::URI", 10) == 0) |
| { |
| gsize expected_size; |
| |
| expected_size = strlen (expected_info->uri); |
| |
| if (expected_size != value_size) |
| return FALSE; |
| |
| if (memcmp (expected_info->uri, value, value_size) != 0) |
| return FALSE; |
| |
| *required_matches |= MATCHED_URI; |
| } |
| |
| else if (key_size == 12 && memcmp (key, "Thumb::MTime", 12) == 0) |
| { |
| if (!check_integer_match (expected_info->mtime, value, value_size)) |
| return FALSE; |
| |
| *required_matches |= MATCHED_MTIME; |
| } |
| |
| else if (key_size == 11 && memcmp (key, "Thumb::Size", 11) == 0) |
| { |
| /* A match on Thumb::Size is not required for success, but if we |
| * find this optional field and it's wrong, we should reject the |
| * thumbnail. |
| */ |
| if (!check_integer_match (expected_info->size, value, value_size)) |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| check_thumbnail_validity (ExpectedInfo *expected_info, |
| const gchar *contents, |
| gsize size) |
| { |
| guint required_matches = 0; |
| |
| /* Reference: http://www.w3.org/TR/PNG/ */ |
| if (size < 8) |
| return FALSE; |
| |
| if (memcmp (contents, "\x89PNG\r\n\x1a\n", 8) != 0) |
| return FALSE; |
| |
| contents += 8, size -= 8; |
| |
| /* We need at least 12 bytes to have a chunk... */ |
| while (size >= 12) |
| { |
| guint32 chunk_size_be; |
| guint32 chunk_size; |
| |
| /* PNG is not an aligned file format so we have to be careful |
| * about reading integers... |
| */ |
| memcpy (&chunk_size_be, contents, 4); |
| chunk_size = GUINT32_FROM_BE (chunk_size_be); |
| |
| contents += 4, size -= 4; |
| |
| /* After consuming the size field, we need to have enough bytes |
| * for 4 bytes type field, chunk_size bytes for data, then 4 byte |
| * for CRC (which we ignore) |
| * |
| * We just read chunk_size from the file, so it may be very large. |
| * Make sure it won't wrap when we add 8 to it. |
| */ |
| if (G_MAXUINT32 - chunk_size < 8 || size < chunk_size + 8) |
| goto out; |
| |
| /* We are only interested in tEXt fields */ |
| if (memcmp (contents, "tEXt", 4) == 0) |
| { |
| const gchar *key = contents + 4; |
| guint32 key_size; |
| |
| /* We need to find the nul separator character that splits the |
| * key/value. The value is not terminated. |
| * |
| * If we find no nul then we just ignore the field. |
| * |
| * value may contain extra nuls, but check_png_info_chunk() |
| * can handle that. |
| */ |
| for (key_size = 0; key_size < chunk_size; key_size++) |
| { |
| if (key[key_size] == '\0') |
| { |
| const gchar *value; |
| guint32 value_size; |
| |
| /* Since key_size < chunk_size, value_size is |
| * definitely non-negative. |
| */ |
| value_size = chunk_size - key_size - 1; |
| value = key + key_size + 1; |
| |
| /* We found the separator character. */ |
| if (!check_png_info_chunk (expected_info, |
| key, key_size, |
| value, value_size, |
| &required_matches)) |
| return FALSE; |
| } |
| } |
| } |
| else |
| { |
| /* A bit of a hack: assume that all tEXt chunks will appear |
| * together. Therefore, if we have already seen both required |
| * fields and then see a non-tEXt chunk then we can assume we |
| * are done. |
| * |
| * The common case is that the tEXt chunks come at the start |
| * of the file before any of the image data. This trick means |
| * that we will only fault in a single page (4k) whereas many |
| * thumbnails (particularly the large ones) can approach 100k |
| * in size. |
| */ |
| if (required_matches == MATCHED_ALL) |
| goto out; |
| } |
| |
| /* skip to the next chunk, ignoring CRC. */ |
| contents += 4, size -= 4; /* type field */ |
| contents += chunk_size, size -= chunk_size; /* data */ |
| contents += 4, size -= 4; /* CRC */ |
| } |
| |
| out: |
| return required_matches == MATCHED_ALL; |
| } |
| |
| gboolean |
| thumbnail_verify (const char *thumbnail_path, |
| const gchar *file_uri, |
| const GLocalFileStat *file_stat_buf) |
| { |
| gboolean thumbnail_is_valid = FALSE; |
| ExpectedInfo expected_info; |
| GMappedFile *file; |
| |
| if (file_stat_buf == NULL) |
| return FALSE; |
| |
| expected_info.uri = file_uri; |
| expected_info.mtime = (guint64) file_stat_buf->st_mtime; |
| expected_info.size = file_stat_buf->st_size; |
| |
| file = g_mapped_file_new (thumbnail_path, FALSE, NULL); |
| if (file) |
| { |
| thumbnail_is_valid = check_thumbnail_validity (&expected_info, |
| g_mapped_file_get_contents (file), |
| g_mapped_file_get_length (file)); |
| g_mapped_file_unref (file); |
| } |
| |
| return thumbnail_is_valid; |
| } |