Trim trailing white spaces on empty lines.
[geeqie.git] / src / cache.c
1 /*
2  * Geeqie
3  * (C) 2004 John Ellis
4  * Copyright (C) 2008 - 2012 The Geeqie Team
5  *
6  * Author: John Ellis
7  *
8  * This software is released under the GNU General Public License (GNU GPL).
9  * Please read the included file COPYING for more information.
10  * This software comes with no warranty of any kind, use at your own risk!
11  */
12
13 #include "main.h"
14 #include "cache.h"
15
16 #include "md5-util.h"
17 #include "secure_save.h"
18 #include "ui_fileops.h"
19
20 #include <utime.h>
21 #include <errno.h>
22
23
24 /*
25  *-------------------------------------------------------------------
26  * Cache data file format:
27  *-------------------------------------------------------------------
28  *
29  * SIMcache
30  * #comment
31  * Dimensions=[<width> x <height>]
32  * Date=[<value in time_t format, or -1 if no embedded date>]
33  * Checksum=[<value>]
34  * MD5sum=[<32 character ascii text digest>]
35  * SimilarityGrid[32 x 32]=<3072 bytes of data (1024 pixels in RGB format, 1 pixel is 24bits)>
36  *
37  *
38  * The first line (9 bytes) indicates it is a SIMcache format file. (new line char must exist)
39  * Comment lines starting with a # are ignored up to a new line.
40  * All data lines should end with a new line char.
41  * Format is very strict, data must begin with the char immediately following '='.
42  * Currently SimilarityGrid is always assumed to be 32 x 32 RGB.
43  */
44
45
46 /*
47  *-------------------------------------------------------------------
48  * sim cache data
49  *-------------------------------------------------------------------
50  */
51
52 CacheData *cache_sim_data_new(void)
53 {
54         CacheData *cd;
55
56         cd = g_new0(CacheData, 1);
57         cd->date = -1;
58
59         return cd;
60 }
61
62 void cache_sim_data_free(CacheData *cd)
63 {
64         if (!cd) return;
65
66         g_free(cd->path);
67         image_sim_free(cd->sim);
68         g_free(cd);
69 }
70
71 /*
72  *-------------------------------------------------------------------
73  * sim cache write
74  *-------------------------------------------------------------------
75  */
76
77 static gboolean cache_sim_write_dimensions(SecureSaveInfo *ssi, CacheData *cd)
78 {
79         if (!cd || !cd->dimensions) return FALSE;
80
81         secure_fprintf(ssi, "Dimensions=[%d x %d]\n", cd->width, cd->height);
82
83         return TRUE;
84 }
85
86 static gboolean cache_sim_write_date(SecureSaveInfo *ssi, CacheData *cd)
87 {
88         if (!cd || !cd->have_date) return FALSE;
89
90         secure_fprintf(ssi, "Date=[%ld]\n", cd->date);
91
92         return TRUE;
93 }
94
95 static gboolean cache_sim_write_checksum(SecureSaveInfo *ssi, CacheData *cd)
96 {
97         if (!cd || !cd->have_checksum) return FALSE;
98
99         secure_fprintf(ssi, "Checksum=[%ld]\n", cd->checksum);
100
101         return TRUE;
102 }
103
104 static gboolean cache_sim_write_md5sum(SecureSaveInfo *ssi, CacheData *cd)
105 {
106         gchar *text;
107
108         if (!cd || !cd->have_md5sum) return FALSE;
109
110         text = md5_digest_to_text(cd->md5sum);
111         secure_fprintf(ssi, "MD5sum=[%s]\n", text);
112         g_free(text);
113
114         return TRUE;
115 }
116
117 static gboolean cache_sim_write_similarity(SecureSaveInfo *ssi, CacheData *cd)
118 {
119         guint x, y;
120         guint8 buf[3 * 32];
121
122         if (!cd || !cd->similarity || !cd->sim || !cd->sim->filled) return FALSE;
123
124         secure_fprintf(ssi, "SimilarityGrid[32 x 32]=");
125         for (y = 0; y < 32; y++)
126                 {
127                 guint s = y * 32;
128                 guint8 *avg_r = &cd->sim->avg_r[s];
129                 guint8 *avg_g = &cd->sim->avg_g[s];
130                 guint8 *avg_b = &cd->sim->avg_b[s];
131                 guint n = 0;
132
133                 for (x = 0; x < 32; x++)
134                         {
135                         buf[n++] = avg_r[x];
136                         buf[n++] = avg_g[x];
137                         buf[n++] = avg_b[x];
138                         }
139
140                 secure_fwrite(buf, sizeof(buf), 1, ssi);
141                 }
142
143         secure_fputc(ssi, '\n');
144
145         return TRUE;
146 }
147
148 gboolean cache_sim_data_save(CacheData *cd)
149 {
150         SecureSaveInfo *ssi;
151         gchar *pathl;
152
153         if (!cd || !cd->path) return FALSE;
154
155         pathl = path_from_utf8(cd->path);
156         ssi = secure_open(pathl);
157         g_free(pathl);
158
159         if (!ssi)
160                 {
161                 log_printf("Unable to save sim cache data: %s\n", cd->path);
162                 return FALSE;
163                 }
164
165         secure_fprintf(ssi, "SIMcache\n#%s %s\n", PACKAGE, VERSION);
166         cache_sim_write_dimensions(ssi, cd);
167         cache_sim_write_date(ssi, cd);
168         cache_sim_write_checksum(ssi, cd);
169         cache_sim_write_md5sum(ssi, cd);
170         cache_sim_write_similarity(ssi, cd);
171
172         if (secure_close(ssi))
173                 {
174                 log_printf(_("error saving sim cache data: %s\nerror: %s\n"), cd->path,
175                             secsave_strerror(secsave_errno));
176                 return FALSE;
177                 }
178
179         return TRUE;
180 }
181
182 /*
183  *-------------------------------------------------------------------
184  * sim cache read
185  *-------------------------------------------------------------------
186  */
187
188 static gboolean cache_sim_read_skipline(FILE *f, gint s)
189 {
190         if (!f) return FALSE;
191
192         if (fseek(f, 0 - s, SEEK_CUR) == 0)
193                 {
194                 gchar b;
195                 while (fread(&b, sizeof(b), 1, f) == 1)
196                         {
197                         if (b == '\n') return TRUE;
198                         }
199                 return TRUE;
200                 }
201
202         return FALSE;
203 }
204
205 static gboolean cache_sim_read_comment(FILE *f, gchar *buf, gint s, CacheData *cd)
206 {
207         if (!f || !buf || !cd) return FALSE;
208
209         if (s < 1 || buf[0] != '#') return FALSE;
210
211         return cache_sim_read_skipline(f, s - 1);
212 }
213
214 static gboolean cache_sim_read_dimensions(FILE *f, gchar *buf, gint s, CacheData *cd)
215 {
216         if (!f || !buf || !cd) return FALSE;
217
218         if (s < 10 || strncmp("Dimensions", buf, 10) != 0) return FALSE;
219
220         if (fseek(f, - s, SEEK_CUR) == 0)
221                 {
222                 gchar b;
223                 gchar buf[1024];
224                 gsize p = 0;
225                 gint w, h;
226
227                 b = 'X';
228                 while (b != '[')
229                         {
230                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
231                         }
232                 while (b != ']' && p < sizeof(buf) - 1)
233                         {
234                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
235                         buf[p] = b;
236                         p++;
237                         }
238
239                 while (b != '\n')
240                         {
241                         if (fread(&b, sizeof(b), 1, f) != 1) break;
242                         }
243
244                 buf[p] = '\0';
245                 if (sscanf(buf, "%d x %d", &w, &h) != 2) return FALSE;
246
247                 cd->width = w;
248                 cd->height = h;
249                 cd->dimensions = TRUE;
250
251                 return TRUE;
252                 }
253
254         return FALSE;
255 }
256
257 static gboolean cache_sim_read_date(FILE *f, gchar *buf, gint s, CacheData *cd)
258 {
259         if (!f || !buf || !cd) return FALSE;
260
261         if (s < 4 || strncmp("Date", buf, 4) != 0) return FALSE;
262
263         if (fseek(f, - s, SEEK_CUR) == 0)
264                 {
265                 gchar b;
266                 gchar buf[1024];
267                 gsize p = 0;
268
269                 b = 'X';
270                 while (b != '[')
271                         {
272                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
273                         }
274                 while (b != ']' && p < sizeof(buf) - 1)
275                         {
276                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
277                         buf[p] = b;
278                         p++;
279                         }
280
281                 while (b != '\n')
282                         {
283                         if (fread(&b, sizeof(b), 1, f) != 1) break;
284                         }
285
286                 buf[p] = '\0';
287                 cd->date = strtol(buf, NULL, 10);
288
289                 cd->have_date = TRUE;
290
291                 return TRUE;
292                 }
293
294         return FALSE;
295 }
296
297 static gboolean cache_sim_read_checksum(FILE *f, gchar *buf, gint s, CacheData *cd)
298 {
299         if (!f || !buf || !cd) return FALSE;
300
301         if (s < 8 || strncmp("Checksum", buf, 8) != 0) return FALSE;
302
303         if (fseek(f, - s, SEEK_CUR) == 0)
304                 {
305                 gchar b;
306                 gchar buf[1024];
307                 gsize p = 0;
308
309                 b = 'X';
310                 while (b != '[')
311                         {
312                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
313                         }
314                 while (b != ']' && p < sizeof(buf) - 1)
315                         {
316                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
317                         buf[p] = b;
318                         p++;
319                         }
320
321                 while (b != '\n')
322                         {
323                         if (fread(&b, sizeof(b), 1, f) != 1) break;
324                         }
325
326                 buf[p] = '\0';
327                 cd->checksum = strtol(buf, NULL, 10);
328
329                 cd->have_checksum = TRUE;
330
331                 return TRUE;
332                 }
333
334         return FALSE;
335 }
336
337 static gboolean cache_sim_read_md5sum(FILE *f, gchar *buf, gint s, CacheData *cd)
338 {
339         if (!f || !buf || !cd) return FALSE;
340
341         if (s < 8 || strncmp("MD5sum", buf, 6) != 0) return FALSE;
342
343         if (fseek(f, - s, SEEK_CUR) == 0)
344                 {
345                 gchar b;
346                 gchar buf[64];
347                 gsize p = 0;
348
349                 b = 'X';
350                 while (b != '[')
351                         {
352                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
353                         }
354                 while (b != ']' && p < sizeof(buf) - 1)
355                         {
356                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
357                         buf[p] = b;
358                         p++;
359                         }
360                 while (b != '\n')
361                         {
362                         if (fread(&b, sizeof(b), 1, f) != 1) break;
363                         }
364
365                 buf[p] = '\0';
366                 cd->have_md5sum = md5_digest_from_text(buf, cd->md5sum);
367
368                 return TRUE;
369                 }
370
371         return FALSE;
372 }
373
374 static gboolean cache_sim_read_similarity(FILE *f, gchar *buf, gint s, CacheData *cd)
375 {
376         if (!f || !buf || !cd) return FALSE;
377
378         if (s < 11 || strncmp("Similarity", buf, 10) != 0) return FALSE;
379
380         if (strncmp("Grid[32 x 32]", buf + 10, 13) != 0) return FALSE;
381
382         if (fseek(f, - s, SEEK_CUR) == 0)
383                 {
384                 gchar b;
385                 guint8 pixel_buf[3];
386                 ImageSimilarityData *sd;
387                 gint x, y;
388
389                 b = 'X';
390                 while (b != '=')
391                         {
392                         if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
393                         }
394
395                 if (cd->sim)
396                         {
397                         /* use current sim that may already contain data we will not touch here */
398                         sd = cd->sim;
399                         cd->sim = NULL;
400                         cd->similarity = FALSE;
401                         }
402                 else
403                         {
404                         sd = image_sim_new();
405                         }
406
407                 for (y = 0; y < 32; y++)
408                         {
409                         gint s = y * 32;
410                         for (x = 0; x < 32; x++)
411                                 {
412                                 if (fread(&pixel_buf, sizeof(pixel_buf), 1, f) != 1)
413                                         {
414                                         image_sim_free(sd);
415                                         return FALSE;
416                                         }
417                                 sd->avg_r[s + x] = pixel_buf[0];
418                                 sd->avg_g[s + x] = pixel_buf[1];
419                                 sd->avg_b[s + x] = pixel_buf[2];
420                                 }
421                         }
422
423                 if (fread(&b, sizeof(b), 1, f) == 1)
424                         {
425                         if (b != '\n') fseek(f, -1, SEEK_CUR);
426                         }
427
428                 cd->sim = sd;
429                 cd->sim->filled = TRUE;
430                 cd->similarity = TRUE;
431
432                 return TRUE;
433                 }
434
435         return FALSE;
436 }
437
438 #define CACHE_LOAD_LINE_NOISE 8
439
440 CacheData *cache_sim_data_load(const gchar *path)
441 {
442         FILE *f;
443         CacheData *cd = NULL;
444         gchar buf[32];
445         gint success = CACHE_LOAD_LINE_NOISE;
446         gchar *pathl;
447
448         if (!path) return NULL;
449
450         pathl = path_from_utf8(path);
451         f = fopen(pathl, "r");
452         g_free(pathl);
453
454         if (!f) return NULL;
455
456         cd = cache_sim_data_new();
457         cd->path = g_strdup(path);
458
459         if (fread(&buf, sizeof(gchar), 9, f) != 9 ||
460             strncmp(buf, "SIMcache", 8) != 0)
461                 {
462                 DEBUG_1("%s is not a cache file", cd->path);
463                 success = 0;
464                 }
465
466         while (success > 0)
467                 {
468                 gint s;
469                 s = fread(&buf, sizeof(gchar), sizeof(buf), f);
470
471                 if (s < 1)
472                         {
473                         success = 0;
474                         }
475                 else
476                         {
477                         if (!cache_sim_read_comment(f, buf, s, cd) &&
478                             !cache_sim_read_dimensions(f, buf, s, cd) &&
479                             !cache_sim_read_date(f, buf, s, cd) &&
480                             !cache_sim_read_checksum(f, buf, s, cd) &&
481                             !cache_sim_read_md5sum(f, buf, s, cd) &&
482                             !cache_sim_read_similarity(f, buf, s, cd))
483                                 {
484                                 if (!cache_sim_read_skipline(f, s))
485                                         {
486                                         success = 0;
487                                         }
488                                 else
489                                         {
490                                         success--;
491                                         }
492                                 }
493                         else
494                                 {
495                                 success = CACHE_LOAD_LINE_NOISE;
496                                 }
497                         }
498                 }
499
500         fclose(f);
501
502         if (!cd->dimensions &&
503             !cd->have_date &&
504             !cd->have_checksum &&
505             !cd->have_md5sum &&
506             !cd->similarity)
507                 {
508                 cache_sim_data_free(cd);
509                 cd = NULL;
510                 }
511
512         return cd;
513 }
514
515 /*
516  *-------------------------------------------------------------------
517  * sim cache setting
518  *-------------------------------------------------------------------
519  */
520
521 void cache_sim_data_set_dimensions(CacheData *cd, gint w, gint h)
522 {
523         if (!cd) return;
524
525         cd->width = w;
526         cd->height = h;
527         cd->dimensions = TRUE;
528 }
529
530 void cache_sim_data_set_date(CacheData *cd, time_t date)
531 {
532         if (!cd) return;
533
534         cd->date = date;
535         cd->have_date = TRUE;
536 }
537
538 void cache_sim_data_set_checksum(CacheData *cd, glong checksum)
539 {
540         if (!cd) return;
541
542         cd->checksum = checksum;
543         cd->have_checksum = TRUE;
544 }
545
546 void cache_sim_data_set_md5sum(CacheData *cd, guchar digest[16])
547 {
548         gint i;
549
550         if (!cd) return;
551
552         for (i = 0; i < 16; i++)
553                 {
554                 cd->md5sum[i] = digest[i];
555                 }
556         cd->have_md5sum = TRUE;
557 }
558
559 void cache_sim_data_set_similarity(CacheData *cd, ImageSimilarityData *sd)
560 {
561         if (!cd || !sd || !sd->filled) return;
562
563         if (!cd->sim) cd->sim = image_sim_new();
564
565         memcpy(cd->sim->avg_r, sd->avg_r, 1024);
566         memcpy(cd->sim->avg_g, sd->avg_g, 1024);
567         memcpy(cd->sim->avg_b, sd->avg_b, 1024);
568         cd->sim->filled = TRUE;
569
570         cd->similarity = TRUE;
571 }
572
573 gboolean cache_sim_data_filled(ImageSimilarityData *sd)
574 {
575         if (!sd) return FALSE;
576         return sd->filled;
577 }
578
579 /*
580  *-------------------------------------------------------------------
581  * cache path location utils
582  *-------------------------------------------------------------------
583  */
584
585
586 static void cache_path_parts(CacheType type,
587                              const gchar **cache_rc, const gchar **cache_local, const gchar **cache_ext)
588 {
589         switch (type)
590                 {
591                 case CACHE_TYPE_THUMB:
592                         *cache_rc = get_thumbnails_cache_dir();
593                         *cache_local = GQ_CACHE_LOCAL_THUMB;
594                         *cache_ext = GQ_CACHE_EXT_THUMB;
595                         break;
596                 case CACHE_TYPE_SIM:
597                         *cache_rc = get_thumbnails_cache_dir();
598                         *cache_local = GQ_CACHE_LOCAL_THUMB;
599                         *cache_ext = GQ_CACHE_EXT_SIM;
600                         break;
601                 case CACHE_TYPE_METADATA:
602                         *cache_rc = get_metadata_cache_dir();
603                         *cache_local = GQ_CACHE_LOCAL_METADATA;
604                         *cache_ext = GQ_CACHE_EXT_METADATA;
605                         break;
606                 case CACHE_TYPE_XMP_METADATA:
607                         *cache_rc = get_metadata_cache_dir();
608                         *cache_local = GQ_CACHE_LOCAL_METADATA;
609                         *cache_ext = GQ_CACHE_EXT_XMP_METADATA;
610                         break;
611                 }
612 }
613
614 gchar *cache_get_location(CacheType type, const gchar *source, gint include_name, mode_t *mode)
615 {
616         gchar *path = NULL;
617         gchar *base;
618         gchar *name = NULL;
619         const gchar *cache_rc;
620         const gchar *cache_local;
621         const gchar *cache_ext;
622
623         if (!source) return NULL;
624
625         cache_path_parts(type, &cache_rc, &cache_local, &cache_ext);
626
627         base = remove_level_from_path(source);
628         if (include_name)
629                 {
630                 name = g_strconcat(filename_from_path(source), cache_ext, NULL);
631                 }
632
633         if (((type != CACHE_TYPE_METADATA && type != CACHE_TYPE_XMP_METADATA && options->thumbnails.cache_into_dirs) ||
634              ((type == CACHE_TYPE_METADATA || type == CACHE_TYPE_XMP_METADATA) && options->metadata.enable_metadata_dirs)) &&
635             access_file(base, W_OK))
636                 {
637                 path = g_build_filename(base, cache_local, name, NULL);
638                 if (mode) *mode = 0775;
639                 }
640
641         if (!path)
642                 {
643                 path = g_build_filename(cache_rc, base, name, NULL);
644                 if (mode) *mode = 0755;
645                 }
646
647         g_free(base);
648         if (name) g_free(name);
649
650         return path;
651 }
652
653 static gchar *cache_build_path_local(const gchar *source, const gchar *cache_local, const gchar *cache_ext)
654 {
655         gchar *path;
656         gchar *base = remove_level_from_path(source);
657         gchar *name = g_strconcat(filename_from_path(source), cache_ext, NULL);
658         path = g_build_filename(base, cache_local, name, NULL);
659         g_free(name);
660         g_free(base);
661
662         return path;
663 }
664
665 static gchar *cache_build_path_rc(const gchar *source, const gchar *cache_rc, const gchar *cache_ext)
666 {
667         gchar *path;
668         gchar *name = g_strconcat(source, cache_ext, NULL);
669         path = g_build_filename(cache_rc, name, NULL);
670         g_free(name);
671
672         return path;
673 }
674
675 gchar *cache_find_location(CacheType type, const gchar *source)
676 {
677         gchar *path;
678         const gchar *cache_rc;
679         const gchar *cache_local;
680         const gchar *cache_ext;
681         gboolean prefer_local;
682
683         if (!source) return NULL;
684
685         cache_path_parts(type, &cache_rc, &cache_local, &cache_ext);
686
687         if (type == CACHE_TYPE_METADATA || type == CACHE_TYPE_XMP_METADATA)
688                 {
689                 prefer_local = options->metadata.enable_metadata_dirs;
690                 }
691         else
692                 {
693                 prefer_local = options->thumbnails.cache_into_dirs;
694                 }
695
696         if (prefer_local)
697                 {
698                 path = cache_build_path_local(source, cache_local, cache_ext);
699                 }
700         else
701                 {
702                 path = cache_build_path_rc(source, cache_rc, cache_ext);
703                 }
704
705         if (!isfile(path))
706                 {
707                 g_free(path);
708
709                 /* try the opposite method if not found */
710                 if (!prefer_local)
711                         {
712                         path = cache_build_path_local(source, cache_local, cache_ext);
713                         }
714                 else
715                         {
716                         path = cache_build_path_rc(source, cache_rc, cache_ext);
717                         }
718
719                 if (!isfile(path))
720                         {
721                         g_free(path);
722                         path = NULL;
723                         }
724                 }
725
726         return path;
727 }
728
729 gboolean cache_time_valid(const gchar *cache, const gchar *path)
730 {
731         struct stat cache_st;
732         struct stat path_st;
733         gchar *cachel;
734         gchar *pathl;
735         gboolean ret = FALSE;
736
737         if (!cache || !path) return FALSE;
738
739         cachel = path_from_utf8(cache);
740         pathl = path_from_utf8(path);
741
742         if (stat(cachel, &cache_st) == 0 &&
743             stat(pathl, &path_st) == 0)
744                 {
745                 if (cache_st.st_mtime == path_st.st_mtime)
746                         {
747                         ret = TRUE;
748                         }
749                 else if (cache_st.st_mtime > path_st.st_mtime)
750                         {
751                         struct utimbuf ut;
752
753                         ut.actime = ut.modtime = cache_st.st_mtime;
754                         if (utime(cachel, &ut) < 0 &&
755                             errno == EPERM)
756                                 {
757                                 DEBUG_1("cache permission workaround: %s", cachel);
758                                 ret = TRUE;
759                                 }
760                         }
761                 }
762
763         g_free(pathl);
764         g_free(cachel);
765
766         return ret;
767 }
768
769 const gchar *get_thumbnails_cache_dir(void)
770 {
771         static gchar *thumbnails_cache_dir = NULL;
772
773         if (thumbnails_cache_dir) return thumbnails_cache_dir;
774
775         if (USE_XDG)
776                 {
777                 thumbnails_cache_dir = g_build_filename(xdg_cache_home_get(), GQ_APPNAME_LC, GQ_CACHE_THUMB, NULL);
778                 }
779         else
780                 {
781                 thumbnails_cache_dir = g_build_filename(get_rc_dir(), GQ_CACHE_THUMB, NULL);
782                 }
783
784         return thumbnails_cache_dir;
785 }
786
787 const gchar *get_metadata_cache_dir(void)
788 {
789         static gchar *metadata_cache_dir = NULL;
790
791         if (metadata_cache_dir) return metadata_cache_dir;
792
793         if (USE_XDG)
794                 {
795                 /* Metadata go to $XDG_DATA_HOME.
796                  * "Keywords and comments, among other things, are irreplaceable and cannot be auto-generated,
797                  * so I don't think they'd be appropriate for the cache directory." -- Omari Stephens on geeqie-devel ml
798                  */
799                 metadata_cache_dir = g_build_filename(xdg_data_home_get(), GQ_APPNAME_LC, GQ_CACHE_METADATA, NULL);
800                 }
801         else
802                 {
803                 metadata_cache_dir = g_build_filename(get_rc_dir(), GQ_CACHE_METADATA, NULL);
804                 }
805
806         return metadata_cache_dir;
807 }
808
809 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */