]> Pileus Git - ~andy/gtk/blob - gtk/updateiconcache.c
#include <config.h>
[~andy/gtk] / gtk / updateiconcache.c
1 /* updateiconcache.c
2  * Copyright (C) 2004  Anders Carlsson <andersca@gnome.org>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public
15  * License along with this library; if not, write to the
16  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17  * Boston, MA 02111-1307, USA.
18  */
19
20 #include <config.h>
21
22 #include <stdlib.h>
23 #include <stdio.h>
24 #include <string.h>
25 #include <sys/types.h>
26 #include <sys/stat.h>
27 #include <unistd.h>
28 #include <errno.h>
29 #include <utime.h>
30
31 #include <glib.h>
32 #include <glib/gstdio.h>
33
34 static gboolean force_update = FALSE;
35 static gboolean quiet = FALSE;
36
37 #define CACHE_NAME "icon-theme.cache"
38
39 #define HAS_SUFFIX_XPM (1 << 0)
40 #define HAS_SUFFIX_SVG (1 << 1)
41 #define HAS_SUFFIX_PNG (1 << 2)
42 #define HAS_ICON_FILE  (1 << 3)
43
44 #define MAJOR_VERSION 1
45 #define MINOR_VERSION 0
46 #define HASH_OFFSET 12
47
48 #define ALIGN_VALUE(this, boundary) \
49   (( ((unsigned long)(this)) + (((unsigned long)(boundary)) -1)) & (~(((unsigned long)(boundary))-1)))
50
51 gboolean
52 is_cache_up_to_date (const gchar *path)
53 {
54   struct stat path_stat, cache_stat;
55   gchar *cache_path;
56   int retval;
57   
58   retval = g_stat (path, &path_stat);
59
60   if (retval < 0)
61     {
62       /* We can't stat the path,
63        * assume we have a updated cache */
64       return TRUE;
65     }
66
67   cache_path = g_build_filename (path, CACHE_NAME, NULL);
68   retval = g_stat (cache_path, &cache_stat);
69   g_free (cache_path);
70   
71   if (retval < 0 && errno == ENOENT)
72     {
73       /* Cache file not found */
74       return FALSE;
75     }
76
77   /* Check mtime */
78   return cache_stat.st_mtime <= path_stat.st_mtime;
79 }
80
81 typedef struct
82 {
83   int flags;
84   int dir_index;
85 } Image;
86
87 static gboolean
88 foreach_remove_func (gpointer key, gpointer value, gpointer user_data)
89 {
90   Image *image = (Image *)value;
91   GHashTable *files = user_data;
92   GList *list;
93   gboolean free_key = FALSE;  
94
95   if (image->flags == HAS_ICON_FILE)
96     {
97       g_free (key);
98       g_free (image);
99
100       return TRUE;
101     }
102
103   list = g_hash_table_lookup (files, key);
104   if (list)
105     free_key = TRUE;
106   
107   list = g_list_prepend (list, value);
108   g_hash_table_insert (files, key, list);
109   
110   if (free_key)
111     g_free (key);
112   
113   return TRUE;
114 }
115
116 GList *
117 scan_directory (const gchar *base_path, 
118                 const gchar *subdir, 
119                 GHashTable  *files, 
120                 GList       *directories,
121                 gint         depth)
122 {
123   GHashTable *dir_hash;
124   GDir *dir;
125   const gchar *name;
126   gchar *dir_path;
127   gboolean dir_added = FALSE;
128   guint dir_index = 0xffff;
129   
130   dir_path = g_build_filename (base_path, subdir, NULL);
131
132   /* FIXME: Use the gerror */
133   dir = g_dir_open (dir_path, 0, NULL);
134   
135   if (!dir)
136     return directories;
137   
138   dir_hash = g_hash_table_new (g_str_hash, g_str_equal);
139
140   while ((name = g_dir_read_name (dir)))
141     {
142       gchar *path;
143       gboolean retval;
144       int flags = 0;
145       Image *image;
146       gchar *basename, *dot;
147
148       path = g_build_filename (dir_path, name, NULL);
149       retval = g_file_test (path, G_FILE_TEST_IS_DIR);
150       if (retval)
151         {
152           gchar *subsubdir;
153
154           if (subdir)
155             subsubdir = g_build_filename (subdir, name, NULL);
156           else
157             subsubdir = g_strdup (name);
158           directories = scan_directory (base_path, subsubdir, files, 
159                                         directories, depth + 1);
160           g_free (subsubdir);
161
162           continue;
163         }
164
165       retval = g_file_test (path, G_FILE_TEST_IS_REGULAR);
166       g_free (path);
167       
168       if (retval)
169         {
170           if (g_str_has_suffix (name, ".png"))
171             flags |= HAS_SUFFIX_PNG;
172           else if (g_str_has_suffix (name, ".svg"))
173             flags |= HAS_SUFFIX_SVG;
174           else if (g_str_has_suffix (name, ".xpm"))
175             flags |= HAS_SUFFIX_XPM;
176           else if (g_str_has_suffix (name, ".icon"))
177             flags |= HAS_ICON_FILE;
178           
179           if (flags == 0)
180             continue;
181           
182           basename = g_strdup (name);
183           dot = strrchr (basename, '.');
184           *dot = '\0';
185           
186           image = g_hash_table_lookup (dir_hash, basename);
187           if (image)
188             image->flags |= flags;
189           else
190             {
191               if (!dir_added) 
192                 {
193                   dir_added = TRUE;
194                   if (subdir)
195                     {
196                       dir_index = g_list_length (directories);
197                       directories = g_list_append (directories, g_strdup (subdir));
198                     }
199                   else
200                     dir_index = 0xffff;
201                 }
202                 
203               image = g_new0 (Image, 1);
204               image->flags = flags;
205               image->dir_index = dir_index;
206               
207               g_hash_table_insert (dir_hash, g_strdup (basename), image);
208             }
209
210           g_free (basename);
211         }
212     }
213
214   g_dir_close (dir);
215
216   /* Move dir into the big file hash */
217   g_hash_table_foreach_remove (dir_hash, foreach_remove_func, files);
218   
219   g_hash_table_destroy (dir_hash);
220
221   return directories;
222 }
223
224 typedef struct _HashNode HashNode;
225
226 struct _HashNode
227 {
228   HashNode *next;
229   gchar *name;
230   GList *image_list;
231 };
232
233 static guint
234 icon_name_hash (gconstpointer key)
235 {
236   const char *p = key;
237   guint h = *p;
238
239   if (h)
240     for (p += 1; *p != '\0'; p++)
241       h = (h << 5) - h + *p;
242
243   return h;
244 }
245
246 typedef struct {
247   gint size;
248   HashNode **nodes;
249 } HashContext;
250
251 static gboolean
252 convert_to_hash (gpointer key, gpointer value, gpointer user_data)
253 {
254   HashContext *context = user_data;
255   guint hash;
256   HashNode *node;
257   
258   hash = icon_name_hash (key) % context->size;
259
260   node = g_new0 (HashNode, 1);
261   node->next = NULL;
262   node->name = key;
263   node->image_list = value;
264
265   if (context->nodes[hash] != NULL)
266     node->next = context->nodes[hash];
267
268   context->nodes[hash] = node;
269   
270   return TRUE;
271 }
272
273 gboolean
274 write_string (FILE *cache, const gchar *n)
275 {
276   gchar *s;
277   int i, l;
278   
279   l = ALIGN_VALUE (strlen (n) + 1, 4);
280   
281   s = g_malloc0 (l);
282   strcpy (s, n);
283
284   i = fwrite (s, l, 1, cache);
285
286   return i == 1;
287   
288 }
289
290 gboolean
291 write_card16 (FILE *cache, guint16 n)
292 {
293   int i;
294   gchar s[2];
295
296   *((guint16 *)s) = GUINT16_TO_BE (n);
297   
298   i = fwrite (s, 2, 1, cache);
299
300   return i == 1;
301 }
302
303 gboolean
304 write_card32 (FILE *cache, guint32 n)
305 {
306   int i;
307   gchar s[4];
308
309   *((guint32 *)s) = GUINT32_TO_BE (n);
310   
311   i = fwrite (s, 4, 1, cache);
312
313   return i == 1;
314 }
315
316 static gboolean
317 write_header (FILE *cache, guint32 dir_list_offset)
318 {
319   return (write_card16 (cache, MAJOR_VERSION) &&
320           write_card16 (cache, MINOR_VERSION) &&
321           write_card32 (cache, HASH_OFFSET) &&
322           write_card32 (cache, dir_list_offset));
323 }
324
325
326 guint
327 get_single_node_size (HashNode *node)
328 {
329   int len = 0;
330
331   /* Node pointers */
332   len += 12;
333
334   /* Name */
335   len += ALIGN_VALUE (strlen (node->name) + 1, 4);
336
337   /* Image list */
338   len += 4 + g_list_length (node->image_list) * 8;
339
340   return len;
341 }
342
343 guint
344 get_bucket_size (HashNode *node)
345 {
346   int len = 0;
347
348   while (node)
349     {
350       len += get_single_node_size (node);
351
352       node = node->next;
353     }
354
355   return len;
356 }
357
358 gboolean
359 write_bucket (FILE *cache, HashNode *node, int *offset)
360 {
361   while (node != NULL)
362     {
363       int next_offset = *offset + get_single_node_size (node);
364       int i, len;
365       GList *list;
366           
367       /* Chain offset */
368       if (node->next != NULL)
369         {
370           if (!write_card32 (cache, next_offset))
371             return FALSE;
372         }
373       else
374         {
375           if (!write_card32 (cache, 0xffffffff))
376             return FALSE;
377         }
378       
379       /* Icon name offset */
380       if (!write_card32 (cache, *offset + 12))
381         return FALSE;
382       
383       /* Image list offset */
384       if (!write_card32 (cache, *offset + 12 + ALIGN_VALUE (strlen (node->name) + 1, 4)))
385         return FALSE;
386       
387       /* Icon name */
388       if (!write_string (cache, node->name))
389         return FALSE;
390       
391       /* Image list */
392       len = g_list_length (node->image_list);
393       if (!write_card32 (cache, len))
394         return FALSE;
395       
396       list = node->image_list;
397       for (i = 0; i < len; i++)
398         {
399           Image *image = list->data;
400           
401           /* Directory index */
402           if (!write_card16 (cache, image->dir_index))
403             return FALSE;
404           
405           /* Flags */
406           if (!write_card16 (cache, image->flags))
407             return FALSE;
408           
409           /* Image data offset */
410           if (!write_card32 (cache, 0))
411             return FALSE;
412           
413           list = list->next;
414         }
415
416       *offset = next_offset;
417       node = node->next;
418     }
419   
420   return TRUE;
421 }
422
423 gboolean
424 write_hash_table (FILE *cache, HashContext *context, int *new_offset)
425 {
426   int offset = HASH_OFFSET;
427   int node_offset;
428   int i;
429
430   if (!(write_card32 (cache, context->size)))
431     return FALSE;
432
433   /* Size int + size * 4 */
434   node_offset = offset + 4 + context->size * 4;
435   
436   for (i = 0; i < context->size; i++)
437     {
438       if (context->nodes[i] != NULL)
439         {
440           if (!write_card32 (cache, node_offset))
441             return FALSE;
442           
443           node_offset += get_bucket_size (context->nodes[i]);
444         }
445       else
446         {
447           if (!write_card32 (cache, 0xffffffff))
448             {
449               return FALSE;
450             }
451         }
452     }
453
454   *new_offset = node_offset;
455
456   /* Now write the buckets */
457   node_offset = offset + 4 + context->size * 4;
458   
459   for (i = 0; i < context->size; i++)
460     {
461       if (!context->nodes[i])
462         continue;
463
464       if (!write_bucket (cache, context->nodes[i], &node_offset))
465         return FALSE;
466     }
467
468   return TRUE;
469 }
470
471 gboolean
472 write_dir_index (FILE *cache, int offset, GList *directories)
473 {
474   int n_dirs;
475   GList *d;
476   char *dir;
477
478   n_dirs = g_list_length (directories);
479
480   if (!write_card32 (cache, n_dirs))
481     return FALSE;
482
483   offset += 4 + n_dirs * 4;
484
485   for (d = directories; d; d = d->next)
486     {
487       dir = d->data;
488       if (!write_card32 (cache, offset))
489         return FALSE;
490       
491       offset += ALIGN_VALUE (strlen (dir) + 1, 4);
492     }
493
494   for (d = directories; d; d = d->next)
495     {
496       dir = d->data;
497
498       if (!write_string (cache, dir))
499         return FALSE;
500     }
501   
502   return TRUE;
503 }
504
505 gboolean
506 write_file (FILE *cache, GHashTable *files, GList *directories)
507 {
508   HashContext context;
509   int new_offset;
510
511   /* Convert the hash table into something looking a bit more
512    * like what we want to write to disk.
513    */
514   context.size = g_spaced_primes_closest (g_hash_table_size (files) / 3);
515   context.nodes = g_new0 (HashNode *, context.size);
516   
517   g_hash_table_foreach_remove (files, convert_to_hash, &context);
518
519   /* Now write the file */
520   /* We write 0 as the directory list offset and go
521    * back and change it later */
522   if (!write_header (cache, 0))
523     {
524       g_printerr ("Failed to write header\n");
525       return FALSE;
526     }
527
528   if (!write_hash_table (cache, &context, &new_offset))
529     {
530       g_printerr ("Failed to write hash table\n");
531       return FALSE;
532     }
533
534   if (!write_dir_index (cache, new_offset, directories))
535     {
536       g_printerr ("Failed to write directory index\n");
537       return FALSE;
538     }
539   
540   rewind (cache);
541
542   if (!write_header (cache, new_offset))
543     {
544       g_printerr ("Failed to rewrite header\n");
545       return FALSE;
546     }
547     
548   return TRUE;
549 }
550
551 void
552 build_cache (const gchar *path)
553 {
554   gchar *cache_path, *tmp_cache_path;
555   GHashTable *files;
556   gboolean retval;
557   FILE *cache;
558   struct stat path_stat, cache_stat;
559   struct utimbuf utime_buf;
560   GList *directories = NULL;
561   
562   tmp_cache_path = g_build_filename (path, "."CACHE_NAME, NULL);
563   cache = g_fopen (tmp_cache_path, "wb");
564   
565   if (!cache)
566     {
567       g_printerr ("Failed to write cache file: %s\n", g_strerror (errno));
568       exit (1);
569     }
570
571   files = g_hash_table_new (g_str_hash, g_str_equal);
572   
573   directories = scan_directory (path, NULL, files, NULL, 0);
574
575   if (g_hash_table_size (files) == 0)
576     {
577       /* Empty table, just close and remove the file */
578
579       fclose (cache);
580       g_unlink (tmp_cache_path);
581       exit (0);
582     }
583     
584   /* FIXME: Handle failure */
585   retval = write_file (cache, files, directories);
586   fclose (cache);
587
588   g_list_foreach (directories, (GFunc)g_free, NULL);
589   g_list_free (directories);
590   
591   if (!retval)
592     {
593       g_unlink (tmp_cache_path);
594       exit (1);
595     }
596
597   cache_path = g_build_filename (path, CACHE_NAME, NULL);
598
599   if (g_rename (tmp_cache_path, cache_path) == -1)
600     {
601       g_unlink (tmp_cache_path);
602       exit (1);
603     }
604
605   /* Update time */
606   /* FIXME: What do do if an error occurs here? */
607   g_stat (path, &path_stat);
608   g_stat (cache_path, &cache_stat);
609
610   utime_buf.actime = path_stat.st_atime;
611   utime_buf.modtime = cache_stat.st_mtime;
612   utime (path, &utime_buf);
613   
614   if (!quiet)
615     g_printerr ("Cache file created successfully.\n");
616 }
617
618 static GOptionEntry args[] = {
619   { "force", 'f', 0, G_OPTION_ARG_NONE, &force_update, "Overwrite an existing cache, even if uptodate", NULL },
620   { "quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet, "Turn off verbose output", NULL },
621   { NULL }
622 };
623
624 int
625 main (int argc, char **argv)
626 {
627   gchar *path;
628   GOptionContext *context;
629
630   if (argc < 2)
631     return 0;
632
633   context = g_option_context_new ("ICONPATH");
634   g_option_context_add_main_entries (context, args, NULL);
635
636   g_option_context_parse (context, &argc, &argv, NULL);
637   
638   path = argv[1];
639 #ifdef G_OS_WIN32
640   path = g_locale_to_utf8 (path, -1, NULL, NULL, NULL);
641 #endif
642   
643   if (!force_update && is_cache_up_to_date (path))
644     return 0;
645
646   build_cache (path);
647   
648   return 0;
649 }