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