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