[glib] file-info: Add a G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID attribute



commit fe7069749fe39a006985ec266260a3c02ee8c855
Author: Ryan Lortie <desrt desrt ca>
Date:   Fri Oct 11 11:22:31 2013 +0100

    file-info: Add a G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID attribute
    
    This indicates whether the thumbnail (given by G_FILE_ATTRIBUTE_THUMBNAIL_PATH)
    is valid — i.e. to represent the file in its current state. If
    G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID is FALSE (for a normal _or_ failed
    thumbnail) it means the file has changed since the thumbnail was generated, and
    the thumbnail is out of date.
    
    Part of checking thumbnail validity (by the spec) involves parsing
    headers out of the thumbnail .png so we include some (small) code to do
    that in a separate file.  We will likely want to copy this code to gvfs
    to do the same for GVfsFile.
    
    Heavily based on a patch from Philip Withnall <philip withnall collabora co uk>
    who suggested the feature and designed the API.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=709898

 docs/reference/gio/Makefile.am      |    3 +-
 docs/reference/gio/gio-sections.txt |    1 +
 gio/Makefile.am                     |    2 +
 gio/gfileattribute.c                |    6 +-
 gio/gfileinfo-priv.h                |    1 +
 gio/gfileinfo.c                     |    1 +
 gio/gfileinfo.h                     |   15 ++
 gio/glocalfileinfo.c                |   36 ++++-
 gio/thumbnail-verify.c              |  250 +++++++++++++++++++++++++++++++++++
 gio/thumbnail-verify.h              |   31 +++++
 10 files changed, 335 insertions(+), 11 deletions(-)
---
diff --git a/docs/reference/gio/Makefile.am b/docs/reference/gio/Makefile.am
index cc97095..a6d5a70 100644
--- a/docs/reference/gio/Makefile.am
+++ b/docs/reference/gio/Makefile.am
@@ -81,7 +81,8 @@ IGNORE_HFILES = \
        gwin32appinfo.h                 \
        gwin32mount.h                   \
        gwin32resolver.h                \
-       gwin32volumemonitor.h
+       gwin32volumemonitor.h           \
+       thumbnail-verify.h
 
 
 # CFLAGS and LDFLAGS for compiling scan program. Only needed
diff --git a/docs/reference/gio/gio-sections.txt b/docs/reference/gio/gio-sections.txt
index f584326..2c9813f 100644
--- a/docs/reference/gio/gio-sections.txt
+++ b/docs/reference/gio/gio-sections.txt
@@ -306,6 +306,7 @@ G_FILE_ATTRIBUTE_OWNER_USER_REAL
 G_FILE_ATTRIBUTE_OWNER_GROUP
 G_FILE_ATTRIBUTE_THUMBNAIL_PATH
 G_FILE_ATTRIBUTE_THUMBNAILING_FAILED
+G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID
 G_FILE_ATTRIBUTE_PREVIEW_ICON
 G_FILE_ATTRIBUTE_FILESYSTEM_SIZE
 G_FILE_ATTRIBUTE_FILESYSTEM_FREE
diff --git a/gio/Makefile.am b/gio/Makefile.am
index 427a392..991fff4 100644
--- a/gio/Makefile.am
+++ b/gio/Makefile.am
@@ -207,6 +207,8 @@ local_sources = \
        gsocks4aproxy.h                 \
        gsocks5proxy.c                  \
        gsocks5proxy.h                  \
+       thumbnail-verify.h              \
+       thumbnail-verify.c              \
        $(NULL)
 
 platform_libadd =
diff --git a/gio/gfileattribute.c b/gio/gfileattribute.c
index eec4595..44a5128 100644
--- a/gio/gfileattribute.c
+++ b/gio/gfileattribute.c
@@ -98,8 +98,9 @@
  * some backend specific data such as a unix UID.</entry></row>
  * <row><entry>"thumbnail"</entry><entry>The "Thumbnail" namespace. Includes
  * information about file thumbnails and their location within the file system. Examples of
- * keys in this namespace include "path" to get the location of a thumbnail, and "failed"
- * to check if thumbnailing of the file failed.</entry></row>
+ * keys in this namespace include "path" to get the location of a thumbnail, "failed"
+ * to check if thumbnailing of the file failed, and "is-valid" to check if the thumbnail is
+ * outdated.</entry></row>
  * <row><entry>"filesystem"</entry><entry>The "Filesystem" namespace. Gets information
  * about the file system where a file is located, such as its type, how much
  * space is left available, and the overall size of the file system.</entry></row>
@@ -191,6 +192,7 @@
  * <row><entry>%G_FILE_ATTRIBUTE_OWNER_GROUP</entry><entry>owner::group</entry><entry>string</entry></row>
  * 
<row><entry>%G_FILE_ATTRIBUTE_THUMBNAIL_PATH</entry><entry>thumbnail::path</entry><entry>bytestring</entry></row>
  * 
<row><entry>%G_FILE_ATTRIBUTE_THUMBNAILING_FAILED</entry><entry>thumbnail::failed</entry><entry>boolean</entry></row>
+ * 
<row><entry>%G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID</entry><entry>thumbnail::is-valid</entry><entry>boolean</entry></row>
  * <row><entry>%G_FILE_ATTRIBUTE_PREVIEW_ICON</entry><entry>preview::icon</entry><entry>object 
(#GIcon)</entry></row>
  * 
<row><entry>%G_FILE_ATTRIBUTE_FILESYSTEM_SIZE</entry><entry>filesystem::size</entry><entry>uint64</entry></row>
  * 
<row><entry>%G_FILE_ATTRIBUTE_FILESYSTEM_FREE</entry><entry>filesystem::free</entry><entry>uint64</entry></row>
diff --git a/gio/gfileinfo-priv.h b/gio/gfileinfo-priv.h
index 0298db5..938ec87 100644
--- a/gio/gfileinfo-priv.h
+++ b/gio/gfileinfo-priv.h
@@ -91,6 +91,7 @@
 #define G_FILE_ATTRIBUTE_ID_OWNER_GROUP (9437184 + 3)
 #define G_FILE_ATTRIBUTE_ID_THUMBNAIL_PATH (10485760 + 1)
 #define G_FILE_ATTRIBUTE_ID_THUMBNAILING_FAILED (10485760 + 2)
+#define G_FILE_ATTRIBUTE_ID_THUMBNAIL_IS_VALID (10485760 + 3)
 #define G_FILE_ATTRIBUTE_ID_PREVIEW_ICON (11534336 + 1)
 #define G_FILE_ATTRIBUTE_ID_FILESYSTEM_SIZE (12582912 + 1)
 #define G_FILE_ATTRIBUTE_ID_FILESYSTEM_FREE (12582912 + 2)
diff --git a/gio/gfileinfo.c b/gio/gfileinfo.c
index 30bed5c..3a2403d 100644
--- a/gio/gfileinfo.c
+++ b/gio/gfileinfo.c
@@ -251,6 +251,7 @@ ensure_attribute_hash (void)
   REGISTER_ATTRIBUTE (OWNER_GROUP);
   REGISTER_ATTRIBUTE (THUMBNAIL_PATH);
   REGISTER_ATTRIBUTE (THUMBNAILING_FAILED);
+  REGISTER_ATTRIBUTE (THUMBNAIL_IS_VALID);
   REGISTER_ATTRIBUTE (PREVIEW_ICON);
   REGISTER_ATTRIBUTE (FILESYSTEM_SIZE);
   REGISTER_ATTRIBUTE (FILESYSTEM_FREE);
diff --git a/gio/gfileinfo.h b/gio/gfileinfo.h
index a001b0d..99f0185 100644
--- a/gio/gfileinfo.h
+++ b/gio/gfileinfo.h
@@ -703,6 +703,21 @@ typedef struct _GFileInfoClass   GFileInfoClass;
  * #GFileAttributeType is %G_FILE_ATTRIBUTE_TYPE_BOOLEAN.
  **/
 #define G_FILE_ATTRIBUTE_THUMBNAILING_FAILED "thumbnail::failed"         /* boolean */
+/**
+ * G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID:
+ *
+ * A key in the "thumbnail" namespace for checking whether the thumbnail is outdated.
+ * This attribute is %TRUE if the thumbnail is up-to-date with the file it represents,
+ * and %FALSE if the file has been modified since the thumbnail was generated.
+ *
+ * If %G_FILE_ATTRIBUTE_THUMBNAILING_FAILED is %TRUE and this attribute is %FALSE,
+ * it indicates that thumbnailing may be attempted again and may succeed.
+ *
+ * Corresponding #GFileAttributeType is %G_FILE_ATTRIBUTE_TYPE_BOOLEAN.
+ *
+ * Since: 2.40
+ */
+#define G_FILE_ATTRIBUTE_THUMBNAIL_IS_VALID "thumbnail::is-valid"        /* boolean */
 
 /* Preview */
 
diff --git a/gio/glocalfileinfo.c b/gio/glocalfileinfo.c
index d7a744f..efcafc3 100644
--- a/gio/glocalfileinfo.c
+++ b/gio/glocalfileinfo.c
@@ -68,6 +68,8 @@
 #endif
 #include "glibintl.h"
 
+#include "thumbnail-verify.h"
+
 #ifdef G_OS_WIN32
 #include <windows.h>
 #include <io.h>
@@ -1278,9 +1280,11 @@ get_content_type (const char          *basename,
   
 }
 
+/* @stat_buf is the pre-calculated result of stat(path), or %NULL if that failed. */
 static void
-get_thumbnail_attributes (const char *path,
-                          GFileInfo  *info)
+get_thumbnail_attributes (const char     *path,
+                          GFileInfo      *info,
+                          const GStatBuf *stat_buf)
 {
   GChecksum *checksum;
   char *uri;
@@ -1291,8 +1295,6 @@ get_thumbnail_attributes (const char *path,
 
   checksum = g_checksum_new (G_CHECKSUM_MD5);
   g_checksum_update (checksum, (const guchar *) uri, strlen (uri));
-  
-  g_free (uri);
 
   basename = g_strconcat (g_checksum_get_string (checksum), ".png", NULL);
   g_checksum_free (checksum);
@@ -1302,7 +1304,11 @@ get_thumbnail_attributes (const char *path,
                                NULL);
 
   if (g_file_test (filename, G_FILE_TEST_IS_REGULAR))
-    _g_file_info_set_attribute_byte_string_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAIL_PATH, filename);
+    {
+      _g_file_info_set_attribute_byte_string_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAIL_PATH, filename);
+      _g_file_info_set_attribute_boolean_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAIL_IS_VALID,
+                                                thumbnail_verify (filename, uri, stat_buf));
+    }
   else
     {
       g_free (filename);
@@ -1311,7 +1317,11 @@ get_thumbnail_attributes (const char *path,
                                    NULL);
 
       if (g_file_test (filename, G_FILE_TEST_IS_REGULAR))
-        _g_file_info_set_attribute_byte_string_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAIL_PATH, filename);
+        {
+          _g_file_info_set_attribute_byte_string_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAIL_PATH, filename);
+          _g_file_info_set_attribute_boolean_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAIL_IS_VALID,
+                                                    thumbnail_verify (filename, uri, stat_buf));
+        }
       else
         {
           g_free (filename);
@@ -1322,11 +1332,16 @@ get_thumbnail_attributes (const char *path,
                                        NULL);
 
           if (g_file_test (filename, G_FILE_TEST_IS_REGULAR))
-            _g_file_info_set_attribute_boolean_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAILING_FAILED, TRUE);
+            {
+              _g_file_info_set_attribute_boolean_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAILING_FAILED, TRUE);
+              _g_file_info_set_attribute_boolean_by_id (info, G_FILE_ATTRIBUTE_ID_THUMBNAIL_IS_VALID,
+                                                        thumbnail_verify (filename, uri, stat_buf));
+            }
         }
     }
   g_free (basename);
   g_free (filename);
+  g_free (uri);
 }
 
 #ifdef G_OS_WIN32
@@ -1935,7 +1950,12 @@ _g_local_file_info_get (const char             *basename,
 
   if (_g_file_attribute_matcher_matches_id (attribute_matcher,
                                            G_FILE_ATTRIBUTE_ID_THUMBNAIL_PATH))
-    get_thumbnail_attributes (path, info);
+    {
+      if (stat_ok)
+          get_thumbnail_attributes (path, info, &statbuf);
+      else
+          get_thumbnail_attributes (path, info, NULL);
+    }
 
   vfs = g_vfs_get_default ();
   class = G_VFS_GET_CLASS (vfs);
diff --git a/gio/thumbnail-verify.c b/gio/thumbnail-verify.c
new file mode 100644
index 0000000..afa10ec
--- /dev/null
+++ b/gio/thumbnail-verify.c
@@ -0,0 +1,250 @@
+/* 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 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * 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 GStatBuf *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 = 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;
+}
diff --git a/gio/thumbnail-verify.h b/gio/thumbnail-verify.h
new file mode 100644
index 0000000..b01fef9
--- /dev/null
+++ b/gio/thumbnail-verify.h
@@ -0,0 +1,31 @@
+/* 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 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt desrt ca>
+ */
+
+#ifndef _thumbnail_verify_h_
+#define _thumbnail_verify_h_
+
+#include <glib.h>
+#include <gstdio.h>
+
+gboolean   thumbnail_verify                   (const gchar            *thumbnail_path,
+                                               const gchar            *file_uri,
+                                               const GStatBuf         *file_stat_buf);
+
+#endif /* _thumbnail_verify_h_ */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]