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