Coverage Report

Created: 2025-08-26 06:31

/src/libsoup/libsoup/cache/soup-cache.c
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2
/*
3
 * soup-cache.c
4
 *
5
 * Copyright (C) 2009, 2010 Igalia S.L.
6
 *
7
 * This library is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU Library General Public
9
 * License as published by the Free Software Foundation; either
10
 * version 2 of the License, or (at your option) any later version.
11
 *
12
 * This library 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 GNU
15
 * Library General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Library General Public License
18
 * along with this library; see the file COPYING.LIB.  If not, write to
19
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20
 * Boston, MA 02110-1301, USA.
21
 */
22
23
/* TODO:
24
 * - Need to hook the feature in the sync SoupSession.
25
 * - Need more tests.
26
 */
27
28
#ifdef HAVE_CONFIG_H
29
#include <config.h>
30
#endif
31
32
#include <string.h>
33
#include <glib/gstdio.h>
34
35
#include "soup-cache.h"
36
#include "soup-body-input-stream.h"
37
#include "soup-cache-client-input-stream.h"
38
#include "soup-cache-input-stream.h"
39
#include "soup-cache-private.h"
40
#include "soup-content-processor.h"
41
#include "soup-message-private.h"
42
#include "soup-message-headers-private.h"
43
#include "soup.h"
44
#include "soup-message-metrics-private.h"
45
#include "soup-misc.h"
46
#include "soup-session-private.h"
47
#include "soup-session-feature-private.h"
48
49
/**
50
 * SoupCache:
51
 *
52
 * File-based cache for HTTP resources.
53
 */
54
55
/**
56
 * SoupCacheability:
57
 * @SOUP_CACHE_CACHEABLE: The message should be cached
58
 * @SOUP_CACHE_UNCACHEABLE: The message shouldn't be cached
59
 * @SOUP_CACHE_INVALIDATES: The messages cache should be invalidated
60
 * @SOUP_CACHE_VALIDATES: The messages cache should be updated
61
 *
62
 * Indicates if a message should or shouldn't be cached.
63
 */
64
65
static void soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
66
67
static SoupContentProcessorInterface *soup_cache_default_content_processor_interface;
68
static void soup_cache_content_processor_init (SoupContentProcessorInterface *interface, gpointer interface_data);
69
70
0
#define DEFAULT_MAX_SIZE (50 * 1024 * 1024)
71
0
#define MAX_ENTRY_DATA_PERCENTAGE 10 /* Percentage of the total size
72
                                  of the cache that can be
73
                                  filled by a single entry */
74
75
/*
76
 * Version 2: cache is now saved in soup.cache2. Added the version
77
 * number to the beginning of the file.
78
 *
79
 * Version 3: added HTTP status code to the cache entries.
80
 *
81
 * Version 4: replaced several types.
82
 *   - freshness_lifetime,corrected_initial_age,response_time: time_t -> guint32
83
 *   - status_code: guint -> guint16
84
 *   - hits: guint -> guint32
85
 *
86
 * Version 5: key is no longer stored on disk as it can be easily
87
 * built from the URI. Apart from that some fields in the
88
 * SoupCacheEntry have changed:
89
 *   - entry key is now a uint32 instead of a (char *).
90
 *   - added uri, used to check for collisions
91
 *   - removed filename, it's built from the entry key.
92
 */
93
0
#define SOUP_CACHE_CURRENT_VERSION 5
94
95
#define OLD_SOUP_CACHE_FILE "soup.cache"
96
0
#define SOUP_CACHE_FILE "soup.cache2"
97
98
0
#define SOUP_CACHE_HEADERS_FORMAT "{ss}"
99
0
#define SOUP_CACHE_PHEADERS_FORMAT "(sbuuuuuqa" SOUP_CACHE_HEADERS_FORMAT ")"
100
0
#define SOUP_CACHE_ENTRIES_FORMAT "(qa" SOUP_CACHE_PHEADERS_FORMAT ")"
101
102
/* Basically the same format than above except that some strings are
103
   prepended with &. This way the GVariant returns a pointer to the
104
   data instead of duplicating the string */
105
#define SOUP_CACHE_DECODE_HEADERS_FORMAT "{&s&s}"
106
107
108
typedef struct _SoupCacheEntry {
109
  guint32 key;
110
  char *uri;
111
  guint32 freshness_lifetime;
112
  gboolean must_revalidate;
113
  gsize length;
114
  guint32 corrected_initial_age;
115
  guint32 response_time;
116
  gboolean dirty;
117
  gboolean being_validated;
118
  SoupMessageHeaders *headers;
119
  guint32 hits;
120
  GCancellable *cancellable;
121
  guint16 status_code;
122
} SoupCacheEntry;
123
124
typedef struct {
125
  char *cache_dir;
126
        GMutex mutex;
127
  GHashTable *cache;
128
  guint n_pending;
129
  SoupSession *session;
130
  SoupCacheType cache_type;
131
  guint size;
132
  guint max_size;
133
  guint max_entry_data_size; /* Computed value. Here for performance reasons */
134
  GList *lru_start;
135
} SoupCachePrivate;
136
137
enum {
138
  PROP_0,
139
  PROP_CACHE_DIR,
140
  PROP_CACHE_TYPE,
141
142
        LAST_PROPERTY
143
};
144
145
static GParamSpec *properties[LAST_PROPERTY] = { NULL, };
146
147
G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT,
148
                         G_ADD_PRIVATE (SoupCache)
149
       G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
150
            soup_cache_session_feature_init)
151
       G_IMPLEMENT_INTERFACE (SOUP_TYPE_CONTENT_PROCESSOR,
152
            soup_cache_content_processor_init))
153
154
static gboolean soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge);
155
static void make_room_for_new_entry (SoupCache *cache, guint length_to_add);
156
static gboolean cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add);
157
158
static GFile *
159
get_file_from_entry (SoupCache *cache, SoupCacheEntry *entry)
160
0
{
161
0
        SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
162
0
  char *filename = g_strdup_printf ("%s%s%u", priv->cache_dir,
163
0
            G_DIR_SEPARATOR_S, (guint) entry->key);
164
0
  GFile *file = g_file_new_for_path (filename);
165
0
  g_free (filename);
166
167
0
  return file;
168
0
}
169
170
static SoupCacheability
171
get_cacheability (SoupCache *cache, SoupMessage *msg)
172
0
{
173
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
174
0
  SoupCacheability cacheability;
175
0
  const char *cache_control, *content_type;
176
0
  gboolean has_max_age = FALSE;
177
178
  /* 1. The request method must be cacheable */
179
0
  if (soup_message_get_method (msg) == SOUP_METHOD_GET)
180
0
    cacheability = SOUP_CACHE_CACHEABLE;
181
0
  else if (soup_message_get_method (msg) == SOUP_METHOD_HEAD ||
182
0
     soup_message_get_method (msg) == SOUP_METHOD_TRACE ||
183
0
     soup_message_get_method (msg) == SOUP_METHOD_CONNECT)
184
0
    return SOUP_CACHE_UNCACHEABLE;
185
0
  else
186
0
    return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
187
188
0
  content_type = soup_message_headers_get_content_type (soup_message_get_response_headers (msg), NULL);
189
0
  if (content_type && !g_ascii_strcasecmp (content_type, "multipart/x-mixed-replace"))
190
0
    return SOUP_CACHE_UNCACHEABLE;
191
192
0
  cache_control = soup_message_headers_get_list_common (soup_message_get_response_headers (msg), SOUP_HEADER_CACHE_CONTROL);
193
0
  if (cache_control && *cache_control) {
194
0
    GHashTable *hash;
195
196
0
    hash = soup_header_parse_param_list (cache_control);
197
198
    /* Shared caches MUST NOT store private resources */
199
0
    if (priv->cache_type == SOUP_CACHE_SHARED) {
200
0
      if (g_hash_table_lookup_extended (hash, "private", NULL, NULL)) {
201
0
        soup_header_free_param_list (hash);
202
0
        return SOUP_CACHE_UNCACHEABLE;
203
0
      }
204
0
    }
205
206
    /* 2. The 'no-store' cache directive does not appear in the
207
     * headers
208
     */
209
0
    if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) {
210
0
      soup_header_free_param_list (hash);
211
0
      return SOUP_CACHE_UNCACHEABLE;
212
0
    }
213
214
0
    if (g_hash_table_lookup_extended (hash, "max-age", NULL, NULL))
215
0
      has_max_age = TRUE;
216
217
    /* This does not appear in section 2.1, but I think it makes
218
     * sense to check it too?
219
     */
220
0
    if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) {
221
0
      soup_header_free_param_list (hash);
222
0
      return SOUP_CACHE_UNCACHEABLE;
223
0
    }
224
225
0
    soup_header_free_param_list (hash);
226
0
  }
227
228
  /* Section 13.9 */
229
0
  if ((g_uri_get_query (soup_message_get_uri (msg))) &&
230
0
      !soup_message_headers_get_one_common (soup_message_get_response_headers (msg), SOUP_HEADER_EXPIRES) &&
231
0
      !has_max_age)
232
0
    return SOUP_CACHE_UNCACHEABLE;
233
234
0
  switch (soup_message_get_status (msg)) {
235
0
  case SOUP_STATUS_PARTIAL_CONTENT:
236
    /* We don't cache partial responses, but they only
237
     * invalidate cached full responses if the headers
238
     * don't match.
239
     */
240
0
    cacheability = SOUP_CACHE_UNCACHEABLE;
241
0
    break;
242
243
0
  case SOUP_STATUS_NOT_MODIFIED:
244
    /* A 304 response validates an existing cache entry */
245
0
    cacheability = SOUP_CACHE_VALIDATES;
246
0
    break;
247
248
0
  case SOUP_STATUS_MULTIPLE_CHOICES:
249
0
  case SOUP_STATUS_MOVED_PERMANENTLY:
250
0
  case SOUP_STATUS_GONE:
251
    /* FIXME: cacheable unless indicated otherwise */
252
0
    cacheability = SOUP_CACHE_UNCACHEABLE;
253
0
    break;
254
255
0
  case SOUP_STATUS_FOUND:
256
0
  case SOUP_STATUS_TEMPORARY_REDIRECT:
257
    /* FIXME: cacheable if explicitly indicated */
258
0
    cacheability = SOUP_CACHE_UNCACHEABLE;
259
0
    break;
260
261
0
  case SOUP_STATUS_SEE_OTHER:
262
0
  case SOUP_STATUS_FORBIDDEN:
263
0
  case SOUP_STATUS_NOT_FOUND:
264
0
  case SOUP_STATUS_METHOD_NOT_ALLOWED:
265
0
    return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
266
267
0
  default:
268
    /* Any 5xx status or any 4xx status not handled above
269
     * is uncacheable but doesn't break the cache.
270
     */
271
0
    if ((soup_message_get_status (msg) >= SOUP_STATUS_BAD_REQUEST &&
272
0
         soup_message_get_status (msg) <= SOUP_STATUS_FAILED_DEPENDENCY) ||
273
0
        soup_message_get_status (msg) >= SOUP_STATUS_INTERNAL_SERVER_ERROR)
274
0
      return SOUP_CACHE_UNCACHEABLE;
275
276
    /* An unrecognized 2xx, 3xx, or 4xx response breaks
277
     * the cache.
278
     */
279
0
    if ((soup_message_get_status (msg) > SOUP_STATUS_PARTIAL_CONTENT &&
280
0
         soup_message_get_status (msg) < SOUP_STATUS_MULTIPLE_CHOICES) ||
281
0
        (soup_message_get_status (msg) > SOUP_STATUS_TEMPORARY_REDIRECT &&
282
0
         soup_message_get_status (msg) < SOUP_STATUS_INTERNAL_SERVER_ERROR))
283
0
      return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
284
0
    break;
285
0
  }
286
287
0
  return cacheability;
288
0
}
289
290
/* NOTE: this function deletes the file pointed by the file argument
291
 * and also unref's the GFile object representing it.
292
 */
293
static void
294
soup_cache_entry_free (SoupCacheEntry *entry)
295
0
{
296
0
  g_free (entry->uri);
297
0
  g_clear_pointer (&entry->headers, soup_message_headers_unref);
298
0
  g_clear_object (&entry->cancellable);
299
300
0
  g_slice_free (SoupCacheEntry, entry);
301
0
}
302
303
static void
304
copy_headers (const char *name, const char *value, SoupMessageHeaders *headers)
305
0
{
306
0
  soup_message_headers_append (headers, name, value);
307
0
}
308
309
static void
310
remove_headers (const char *name, const char *value, SoupMessageHeaders *headers)
311
0
{
312
0
  soup_message_headers_remove (headers, name);
313
0
}
314
315
static SoupHeaderName hop_by_hop_headers[] = {
316
        SOUP_HEADER_CONNECTION,
317
        SOUP_HEADER_KEEP_ALIVE,
318
        SOUP_HEADER_PROXY_AUTHENTICATE,
319
        SOUP_HEADER_PROXY_AUTHORIZATION,
320
        SOUP_HEADER_TE,
321
        SOUP_HEADER_TRAILER,
322
        SOUP_HEADER_TRANSFER_ENCODING,
323
        SOUP_HEADER_UPGRADE
324
};
325
326
static void
327
copy_end_to_end_headers (SoupMessageHeaders *source, SoupMessageHeaders *destination)
328
0
{
329
0
  int i;
330
331
0
  soup_message_headers_foreach (source, (SoupMessageHeadersForeachFunc) copy_headers, destination);
332
0
  for (i = 0; i < G_N_ELEMENTS (hop_by_hop_headers); i++)
333
0
    soup_message_headers_remove_common (destination, hop_by_hop_headers[i]);
334
0
  soup_message_headers_clean_connection_headers (destination);
335
0
}
336
337
static guint
338
soup_cache_entry_get_current_age (SoupCacheEntry *entry)
339
0
{
340
0
  time_t now = time (NULL);
341
0
  time_t resident_time;
342
343
0
  resident_time = now - entry->response_time;
344
0
  return entry->corrected_initial_age + resident_time;
345
0
}
346
347
static gboolean
348
soup_cache_entry_is_fresh_enough (SoupCacheEntry *entry, gint min_fresh)
349
0
{
350
0
  guint limit = (min_fresh == -1) ? soup_cache_entry_get_current_age (entry) : (guint) min_fresh;
351
0
  return entry->freshness_lifetime > limit;
352
0
}
353
354
static inline guint32
355
get_cache_key_from_uri (const char *uri)
356
0
{
357
0
  return (guint32) g_str_hash (uri);
358
0
}
359
360
static void
361
soup_cache_entry_set_freshness (SoupCacheEntry *entry, SoupMessage *msg, SoupCache *cache)
362
0
{
363
0
  const char *cache_control;
364
0
  const char *expires, *date, *last_modified;
365
366
  /* Reset these values. We have to do this to ensure that
367
   * revalidations overwrite previous values for the headers.
368
   */
369
0
  entry->must_revalidate = FALSE;
370
0
  entry->freshness_lifetime = 0;
371
372
0
  cache_control = soup_message_headers_get_list_common (entry->headers, SOUP_HEADER_CACHE_CONTROL);
373
0
  if (cache_control && *cache_control) {
374
0
    const char *max_age, *s_maxage;
375
0
    gint64 freshness_lifetime = 0;
376
0
    GHashTable *hash;
377
0
    SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
378
379
0
    hash = soup_header_parse_param_list (cache_control);
380
381
    /* Should we re-validate the entry when it goes stale */
382
0
    entry->must_revalidate = g_hash_table_lookup_extended (hash, "must-revalidate", NULL, NULL);
383
384
    /* Section 2.3.1 */
385
0
    if (priv->cache_type == SOUP_CACHE_SHARED) {
386
0
      s_maxage = g_hash_table_lookup (hash, "s-maxage");
387
0
      if (s_maxage) {
388
0
        freshness_lifetime = g_ascii_strtoll (s_maxage, NULL, 10);
389
0
        if (freshness_lifetime) {
390
          /* Implies proxy-revalidate. TODO: is it true? */
391
0
          entry->must_revalidate = TRUE;
392
0
          soup_header_free_param_list (hash);
393
0
          return;
394
0
        }
395
0
      }
396
0
    }
397
398
    /* If 'max-age' cache directive is present, use that */
399
0
    max_age = g_hash_table_lookup (hash, "max-age");
400
0
    if (max_age)
401
0
      freshness_lifetime = g_ascii_strtoll (max_age, NULL, 10);
402
403
0
    if (freshness_lifetime) {
404
0
      entry->freshness_lifetime = (guint32) MIN (freshness_lifetime, G_MAXUINT32);
405
0
      soup_header_free_param_list (hash);
406
0
      return;
407
0
    }
408
409
0
    soup_header_free_param_list (hash);
410
0
  }
411
412
  /* If the 'Expires' response header is present, use its value
413
   * minus the value of the 'Date' response header
414
   */
415
0
  expires = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_EXPIRES);
416
0
  date = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_DATE);
417
0
  if (expires && date) {
418
0
    GDateTime *expires_d, *date_d;
419
0
    gint64 expires_t, date_t;
420
421
0
    expires_d = soup_date_time_new_from_http_string (expires);
422
0
    if (expires_d) {
423
0
      date_d = soup_date_time_new_from_http_string (date);
424
425
0
      expires_t = g_date_time_to_unix (expires_d);
426
0
      date_t = g_date_time_to_unix (date_d);
427
428
0
      g_date_time_unref (expires_d);
429
0
      g_date_time_unref (date_d);
430
431
0
      if (expires_t && date_t) {
432
0
        entry->freshness_lifetime = (guint32) MAX (expires_t - date_t, 0);
433
0
        return;
434
0
      }
435
0
    } else {
436
      /* If Expires is not a valid date we should
437
         treat it as already expired, see section
438
         3.3 */
439
0
      entry->freshness_lifetime = 0;
440
0
      return;
441
0
    }
442
0
  }
443
444
  /* Otherwise an heuristic may be used */
445
446
  /* Heuristics MUST NOT be used with stored responses with
447
     these status codes (section 2.3.1.1) */
448
0
  if (entry->status_code != SOUP_STATUS_OK &&
449
0
      entry->status_code != SOUP_STATUS_NON_AUTHORITATIVE &&
450
0
      entry->status_code != SOUP_STATUS_PARTIAL_CONTENT &&
451
0
      entry->status_code != SOUP_STATUS_MULTIPLE_CHOICES &&
452
0
      entry->status_code != SOUP_STATUS_MOVED_PERMANENTLY &&
453
0
      entry->status_code != SOUP_STATUS_GONE)
454
0
    goto expire;
455
456
  /* TODO: attach warning 113 if response's current_age is more
457
     than 24h (section 2.3.1.1) when using heuristics */
458
459
  /* Last-Modified based heuristic */
460
0
  last_modified = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_LAST_MODIFIED);
461
0
  if (last_modified) {
462
0
    GDateTime *soup_date;
463
0
    gint64 now, last_modified_t;
464
465
0
    soup_date = soup_date_time_new_from_http_string (last_modified);
466
0
    last_modified_t = g_date_time_to_unix (soup_date);
467
0
    now = time (NULL);
468
469
0
#define HEURISTIC_FACTOR 0.1 /* From Section 2.3.1.1 */
470
471
0
    entry->freshness_lifetime = MAX (0, (now - last_modified_t) * HEURISTIC_FACTOR);
472
0
    g_date_time_unref (soup_date);
473
0
  }
474
475
0
  return;
476
477
0
 expire:
478
  /* If all else fails, make the entry expire immediately */
479
0
  entry->freshness_lifetime = 0;
480
0
}
481
482
static SoupCacheEntry *
483
soup_cache_entry_new (SoupCache *cache, SoupMessage *msg, time_t request_time, time_t response_time)
484
0
{
485
0
  SoupCacheEntry *entry;
486
0
  const char *date;
487
488
0
  entry = g_slice_new0 (SoupCacheEntry);
489
0
  entry->dirty = FALSE;
490
0
  entry->being_validated = FALSE;
491
0
  entry->status_code = soup_message_get_status (msg);
492
0
  entry->response_time = response_time;
493
0
  entry->uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD);
494
495
  /* Headers */
496
0
  entry->headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
497
0
  copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers);
498
499
  /* LRU list */
500
0
  entry->hits = 0;
501
502
  /* Section 2.3.1, Freshness Lifetime */
503
0
  soup_cache_entry_set_freshness (entry, msg, cache);
504
505
  /* Section 2.3.2, Calculating Age */
506
0
  date = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_DATE);
507
508
0
  if (date) {
509
0
    GDateTime *soup_date;
510
0
    const char *age;
511
0
    gint64 date_value, apparent_age, corrected_received_age, response_delay, age_value = 0;
512
513
0
    soup_date = soup_date_time_new_from_http_string (date);
514
0
    date_value = g_date_time_to_unix (soup_date);
515
0
    g_date_time_unref (soup_date);
516
517
0
    age = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_AGE);
518
0
    if (age)
519
0
      age_value = g_ascii_strtoll (age, NULL, 10);
520
521
0
    apparent_age = MAX (0, entry->response_time - date_value);
522
0
    corrected_received_age = MAX (apparent_age, age_value);
523
0
    response_delay = entry->response_time - request_time;
524
0
    entry->corrected_initial_age = corrected_received_age + response_delay;
525
0
  } else {
526
    /* Is this correct ? */
527
0
    entry->corrected_initial_age = time (NULL);
528
0
  }
529
530
0
  return entry;
531
0
}
532
533
static gboolean
534
soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge)
535
0
{
536
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
537
0
  GList *lru_item;
538
539
0
  if (entry->dirty) {
540
0
    g_cancellable_cancel (entry->cancellable);
541
0
    return FALSE;
542
0
  }
543
544
0
  g_assert (!entry->dirty);
545
0
  g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache));
546
547
0
  if (!g_hash_table_remove (priv->cache, GUINT_TO_POINTER (entry->key))) {
548
0
                g_mutex_unlock (&priv->mutex);
549
0
    return FALSE;
550
0
        }
551
552
  /* Remove from LRU */
553
0
  lru_item = g_list_find (priv->lru_start, entry);
554
0
  priv->lru_start = g_list_delete_link (priv->lru_start, lru_item);
555
556
  /* Adjust cache size */
557
0
  priv->size -= entry->length;
558
559
0
  g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache));
560
561
  /* Free resources */
562
0
  if (purge) {
563
0
    GFile *file = get_file_from_entry (cache, entry);
564
0
    g_file_delete (file, NULL, NULL);
565
0
    g_object_unref (file);
566
0
  }
567
0
  soup_cache_entry_free (entry);
568
569
0
  return TRUE;
570
0
}
571
572
static gint
573
lru_compare_func (gconstpointer a, gconstpointer b)
574
0
{
575
0
  SoupCacheEntry *entry_a = (SoupCacheEntry *)a;
576
0
  SoupCacheEntry *entry_b = (SoupCacheEntry *)b;
577
578
  /* The rationale of this sorting func is
579
   *
580
   * 1. sort by hits -> LRU algorithm, then
581
   *
582
   * 2. sort by freshness lifetime, we better discard first
583
   * entries that are close to expire
584
   *
585
   * 3. sort by size, replace first small size resources as they
586
   * are cheaper to download
587
   */
588
589
  /* Sort by hits */
590
0
  if (entry_a->hits != entry_b->hits)
591
0
    return entry_a->hits - entry_b->hits;
592
593
  /* Sort by freshness_lifetime */
594
0
  if (entry_a->freshness_lifetime != entry_b->freshness_lifetime)
595
0
    return entry_a->freshness_lifetime - entry_b->freshness_lifetime;
596
597
  /* Sort by size */
598
0
  return entry_a->length - entry_b->length;
599
0
}
600
601
static gboolean
602
cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add)
603
0
{
604
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
605
  /* We could add here some more heuristics. TODO: review how
606
     this is done by other HTTP caches */
607
608
0
  return length_to_add <= priv->max_entry_data_size;
609
0
}
610
611
static void
612
make_room_for_new_entry (SoupCache *cache, guint length_to_add)
613
0
{
614
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
615
0
  GList *lru_entry = priv->lru_start;
616
617
  /* Check that there is enough room for the new entry. This is
618
     an approximation as we're not working out the size of the
619
     cache file or the size of the headers for performance
620
     reasons. TODO: check if that would be really that expensive */
621
622
0
  while (lru_entry &&
623
0
         (length_to_add + priv->size > priv->max_size)) {
624
0
    SoupCacheEntry *old_entry = (SoupCacheEntry *)lru_entry->data;
625
626
    /* Discard entries. Once cancelled resources will be
627
     * freed in close_ready_cb
628
     */
629
0
    if (soup_cache_entry_remove (cache, old_entry, TRUE))
630
0
      lru_entry = priv->lru_start;
631
0
    else
632
0
      lru_entry = g_list_next (lru_entry);
633
0
  }
634
0
}
635
636
static gboolean
637
soup_cache_entry_insert (SoupCache *cache,
638
       SoupCacheEntry *entry,
639
       gboolean sort)
640
0
{
641
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
642
0
  guint length_to_add = 0;
643
0
  SoupCacheEntry *old_entry;
644
645
  /* Fill the key */
646
0
  entry->key = get_cache_key_from_uri ((const char *) entry->uri);
647
648
0
  if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH)
649
0
    length_to_add = soup_message_headers_get_content_length (entry->headers);
650
651
  /* Check if we are going to store the resource depending on its size */
652
0
  if (length_to_add) {
653
0
    if (!cache_accepts_entries_of_size (cache, length_to_add))
654
0
      return FALSE;
655
656
    /* Make room for new entry if needed */
657
0
    make_room_for_new_entry (cache, length_to_add);
658
0
  }
659
660
  /* Remove any previous entry */
661
0
  if ((old_entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (entry->key))) != NULL) {
662
0
    if (!soup_cache_entry_remove (cache, old_entry, TRUE))
663
0
      return FALSE;
664
0
  }
665
666
  /* Add to hash table */
667
0
  g_hash_table_insert (priv->cache, GUINT_TO_POINTER (entry->key), entry);
668
669
  /* Compute new cache size */
670
0
  priv->size += length_to_add;
671
672
  /* Update LRU */
673
0
  if (sort)
674
0
    priv->lru_start = g_list_insert_sorted (priv->lru_start, entry, lru_compare_func);
675
0
  else
676
0
    priv->lru_start = g_list_prepend (priv->lru_start, entry);
677
678
0
  g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache));
679
680
0
  return TRUE;
681
0
}
682
683
static SoupCacheEntry*
684
soup_cache_entry_lookup (SoupCache *cache,
685
       SoupMessage *msg)
686
0
{
687
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
688
0
  SoupCacheEntry *entry;
689
0
  guint32 key;
690
0
  char *uri = NULL;
691
692
0
  uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD);
693
0
  key = get_cache_key_from_uri ((const char *) uri);
694
695
0
  entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (key));
696
697
0
  if (entry != NULL && (strcmp (entry->uri, uri) != 0))
698
0
    entry = NULL;
699
700
0
  g_free (uri);
701
0
  return entry;
702
0
}
703
704
GInputStream *
705
soup_cache_send_response (SoupCache *cache, SoupMessage *msg)
706
0
{
707
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
708
0
  SoupCacheEntry *entry;
709
0
  GInputStream *file_stream, *body_stream, *cache_stream, *client_stream;
710
0
  GFile *file;
711
0
        SoupMessageMetrics *metrics;
712
713
0
  g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL);
714
0
  g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL);
715
716
0
        soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_REQUEST_START);
717
718
0
        g_mutex_lock (&priv->mutex);
719
0
  entry = soup_cache_entry_lookup (cache, msg);
720
0
        g_mutex_unlock (&priv->mutex);
721
0
  g_return_val_if_fail (entry, NULL);
722
723
0
  file = get_file_from_entry (cache, entry);
724
0
  file_stream = G_INPUT_STREAM (g_file_read (file, NULL, NULL));
725
0
  g_object_unref (file);
726
727
  /* Do not change the original message if there is no resource */
728
0
  if (!file_stream)
729
0
    return NULL;
730
731
0
  body_stream = soup_body_input_stream_new (file_stream, SOUP_ENCODING_CONTENT_LENGTH, entry->length);
732
0
  g_object_unref (file_stream);
733
734
0
  if (!body_stream)
735
0
    return NULL;
736
737
0
        metrics = soup_message_get_metrics (msg);
738
0
        if (metrics)
739
0
                metrics->response_body_size = entry->length;
740
741
  /* If we are told to send a response from cache any validation
742
     in course is over by now */
743
0
  entry->being_validated = FALSE;
744
745
  /* Message starting */
746
0
  soup_message_starting (msg);
747
748
0
        soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_RESPONSE_START);
749
750
  /* Status */
751
0
  soup_message_set_status (msg, entry->status_code, NULL);
752
753
  /* Headers */
754
0
  copy_end_to_end_headers (entry->headers, soup_message_get_response_headers (msg));
755
756
  /* Create the cache stream. */
757
0
  soup_message_disable_feature (msg, SOUP_TYPE_CACHE);
758
0
  cache_stream = soup_session_setup_message_body_input_stream (priv->session,
759
0
                                                                     msg, body_stream,
760
0
                                                                     SOUP_STAGE_ENTITY_BODY);
761
0
  g_object_unref (body_stream);
762
763
0
  client_stream = soup_cache_client_input_stream_new (cache_stream);
764
0
  g_object_unref (cache_stream);
765
766
0
  return client_stream;
767
0
}
768
769
static void
770
msg_got_headers_cb (SoupMessage *msg, gpointer user_data)
771
0
{
772
0
  g_object_set_data (G_OBJECT (msg), "response-time", GINT_TO_POINTER (time (NULL)));
773
0
  g_signal_handlers_disconnect_by_func (msg, msg_got_headers_cb, user_data);
774
0
}
775
776
static void
777
msg_starting_cb (SoupMessage *msg, gpointer user_data)
778
0
{
779
0
  g_object_set_data (G_OBJECT (msg), "request-time", GINT_TO_POINTER (time (NULL)));
780
0
  g_signal_connect (msg, "got-headers", G_CALLBACK (msg_got_headers_cb), user_data);
781
0
  g_signal_handlers_disconnect_by_func (msg, msg_starting_cb, user_data);
782
0
}
783
784
static void
785
request_queued (SoupSessionFeature *feature,
786
    SoupMessage        *msg)
787
0
{
788
0
  g_signal_connect (msg, "starting", G_CALLBACK (msg_starting_cb), feature);
789
0
}
790
791
static void
792
attach (SoupSessionFeature *feature, SoupSession *session)
793
0
{
794
0
  SoupCache *cache = SOUP_CACHE (feature);
795
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
796
0
  priv->session = session;
797
0
}
798
799
static void
800
soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface,
801
          gpointer interface_data)
802
0
{
803
0
  feature_interface->attach = attach;
804
0
  feature_interface->request_queued = request_queued;
805
0
}
806
807
typedef struct {
808
  SoupCache *cache;
809
  SoupCacheEntry *entry;
810
} StreamHelper;
811
812
static void
813
istream_caching_finished (SoupCacheInputStream *istream,
814
        gsize                 bytes_written,
815
        GError               *error,
816
        gpointer              user_data)
817
0
{
818
0
  StreamHelper *helper = (StreamHelper *) user_data;
819
0
  SoupCache *cache = helper->cache;
820
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
821
0
  SoupCacheEntry *entry = helper->entry;
822
823
0
        g_mutex_lock (&priv->mutex);
824
825
0
  --priv->n_pending;
826
827
0
  entry->dirty = FALSE;
828
0
  entry->length = bytes_written;
829
0
  g_clear_object (&entry->cancellable);
830
831
0
  if (error) {
832
    /* Update cache size */
833
0
    if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH)
834
0
      priv->size -= soup_message_headers_get_content_length (entry->headers);
835
836
0
    soup_cache_entry_remove (cache, entry, TRUE);
837
0
    helper->entry = entry = NULL;
838
0
    goto cleanup;
839
0
  }
840
841
0
  if (soup_message_headers_get_encoding (entry->headers) != SOUP_ENCODING_CONTENT_LENGTH) {
842
843
0
    if (cache_accepts_entries_of_size (cache, entry->length)) {
844
0
      make_room_for_new_entry (cache, entry->length);
845
0
      priv->size += entry->length;
846
0
    } else {
847
0
      soup_cache_entry_remove (cache, entry, TRUE);
848
0
      helper->entry = entry = NULL;
849
0
    }
850
0
  }
851
852
0
 cleanup:
853
0
        g_mutex_unlock (&priv->mutex);
854
0
  g_object_unref (helper->cache);
855
0
  g_slice_free (StreamHelper, helper);
856
0
}
857
858
static GInputStream*
859
soup_cache_content_processor_wrap_input (SoupContentProcessor *processor,
860
           GInputStream *base_stream,
861
           SoupMessage *msg,
862
           GError **error)
863
0
{
864
0
  SoupCache *cache = (SoupCache*) processor;
865
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
866
0
  SoupCacheEntry *entry;
867
0
  SoupCacheability cacheability;
868
0
  GInputStream *istream;
869
0
  GFile *file;
870
0
  StreamHelper *helper;
871
0
  time_t request_time, response_time;
872
873
0
        g_mutex_lock (&priv->mutex);
874
875
  /* First of all, check if we should cache the resource. */
876
0
  cacheability = soup_cache_get_cacheability (cache, msg);
877
0
  entry = soup_cache_entry_lookup (cache, msg);
878
879
0
  if (cacheability & SOUP_CACHE_INVALIDATES) {
880
0
    if (entry)
881
0
      soup_cache_entry_remove (cache, entry, TRUE);
882
0
                g_mutex_unlock (&priv->mutex);
883
0
    return NULL;
884
0
  }
885
886
0
  if (cacheability & SOUP_CACHE_VALIDATES) {
887
    /* It's possible to get a CACHE_VALIDATES with no
888
     * entry in the hash table. This could happen if for
889
     * example the soup client is the one creating the
890
     * conditional request.
891
     */
892
0
    if (entry)
893
0
      soup_cache_update_from_conditional_request (cache, msg);
894
0
                g_mutex_unlock (&priv->mutex);
895
0
    return NULL;
896
0
  }
897
898
0
  if (!(cacheability & SOUP_CACHE_CACHEABLE)) {
899
0
                g_mutex_unlock (&priv->mutex);
900
0
    return NULL;
901
0
        }
902
903
  /* Check if we are already caching this resource */
904
0
  if (entry && (entry->dirty || entry->being_validated)) {
905
0
                g_mutex_unlock (&priv->mutex);
906
0
    return NULL;
907
0
        }
908
909
  /* Create a new entry, deleting any old one if present */
910
0
  if (entry)
911
0
    soup_cache_entry_remove (cache, entry, TRUE);
912
913
0
  request_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "request-time"));
914
0
  response_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "response-time"));
915
0
  entry = soup_cache_entry_new (cache, msg, request_time, response_time);
916
0
  entry->hits = 1;
917
0
  entry->dirty = TRUE;
918
919
  /* Do not continue if it can not be stored */
920
0
  if (!soup_cache_entry_insert (cache, entry, TRUE)) {
921
0
    soup_cache_entry_free (entry);
922
0
                g_mutex_unlock (&priv->mutex);
923
0
    return NULL;
924
0
  }
925
926
0
  entry->cancellable = g_cancellable_new ();
927
0
  ++priv->n_pending;
928
929
0
        g_mutex_unlock (&priv->mutex);
930
931
0
  helper = g_slice_new (StreamHelper);
932
0
  helper->cache = g_object_ref (cache);
933
0
  helper->entry = entry;
934
935
0
  file = get_file_from_entry (cache, entry);
936
0
  istream = soup_cache_input_stream_new (base_stream, file);
937
0
  g_object_unref (file);
938
939
0
  g_signal_connect (istream, "caching-finished", G_CALLBACK (istream_caching_finished), helper);
940
941
0
  return istream;
942
0
}
943
944
static void
945
soup_cache_content_processor_init (SoupContentProcessorInterface *processor_interface,
946
           gpointer interface_data)
947
0
{
948
0
  soup_cache_default_content_processor_interface =
949
0
    g_type_default_interface_peek (SOUP_TYPE_CONTENT_PROCESSOR);
950
951
0
  processor_interface->processing_stage = SOUP_STAGE_ENTITY_BODY;
952
0
  processor_interface->wrap_input = soup_cache_content_processor_wrap_input;
953
0
}
954
955
static void
956
soup_cache_init (SoupCache *cache)
957
0
{
958
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
959
960
0
  priv->cache = g_hash_table_new (g_direct_hash, g_direct_equal);
961
  /* LRU */
962
0
  priv->lru_start = NULL;
963
964
  /* */
965
0
  priv->n_pending = 0;
966
967
  /* Cache size */
968
0
  priv->max_size = DEFAULT_MAX_SIZE;
969
0
  priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE;
970
0
  priv->size = 0;
971
972
0
        g_mutex_init (&priv->mutex);
973
0
}
974
975
static void
976
remove_cache_item (gpointer data,
977
       gpointer user_data)
978
0
{
979
0
  soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, FALSE);
980
0
}
981
982
static void
983
soup_cache_finalize (GObject *object)
984
0
{
985
0
  SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object);
986
0
  GList *entries;
987
988
  /* Cannot use g_hash_table_foreach as callbacks must not modify the hash table */
989
0
  entries = g_hash_table_get_values (priv->cache);
990
0
  g_list_foreach (entries, remove_cache_item, object);
991
0
  g_list_free (entries);
992
993
0
  g_hash_table_destroy (priv->cache);
994
0
  g_free (priv->cache_dir);
995
996
0
  g_list_free (priv->lru_start);
997
998
0
        g_mutex_clear (&priv->mutex);
999
1000
0
  G_OBJECT_CLASS (soup_cache_parent_class)->finalize (object);
1001
0
}
1002
1003
static void
1004
soup_cache_set_property (GObject *object, guint prop_id,
1005
        const GValue *value, GParamSpec *pspec)
1006
0
{
1007
0
  SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object);
1008
1009
0
  switch (prop_id) {
1010
0
  case PROP_CACHE_DIR:
1011
0
    g_assert (!priv->cache_dir);
1012
1013
0
    priv->cache_dir = g_value_dup_string (value);
1014
1015
0
    if (!priv->cache_dir)
1016
      /* Set a default cache dir, different for each user */
1017
0
      priv->cache_dir = g_build_filename (g_get_user_cache_dir (),
1018
0
                  "httpcache",
1019
0
                  NULL);
1020
1021
    /* Create directory if it does not exist */
1022
0
    if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))
1023
0
      g_mkdir_with_parents (priv->cache_dir, 0700);
1024
0
    break;
1025
0
  case PROP_CACHE_TYPE:
1026
0
    priv->cache_type = g_value_get_enum (value);
1027
    /* TODO: clear private entries and issue a warning if moving to shared? */
1028
0
    break;
1029
0
  default:
1030
0
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1031
0
    break;
1032
0
  }
1033
0
}
1034
1035
static void
1036
soup_cache_get_property (GObject *object, guint prop_id,
1037
       GValue *value, GParamSpec *pspec)
1038
0
{
1039
0
  SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object);
1040
1041
0
  switch (prop_id) {
1042
0
  case PROP_CACHE_DIR:
1043
0
    g_value_set_string (value, priv->cache_dir);
1044
0
    break;
1045
0
  case PROP_CACHE_TYPE:
1046
0
    g_value_set_enum (value, priv->cache_type);
1047
0
    break;
1048
0
  default:
1049
0
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1050
0
    break;
1051
0
  }
1052
0
}
1053
1054
static void
1055
soup_cache_class_init (SoupCacheClass *cache_class)
1056
0
{
1057
0
  GObjectClass *gobject_class = (GObjectClass *)cache_class;
1058
1059
0
  gobject_class->finalize = soup_cache_finalize;
1060
0
  gobject_class->set_property = soup_cache_set_property;
1061
0
  gobject_class->get_property = soup_cache_get_property;
1062
1063
0
  cache_class->get_cacheability = get_cacheability;
1064
1065
        /**
1066
         * SoupCache:cache-dir:
1067
         * The directory to store the cache files.
1068
         */
1069
0
        properties[PROP_CACHE_DIR] =
1070
0
                g_param_spec_string ("cache-dir",
1071
0
                                     "Cache directory",
1072
0
                                     "The directory to store the cache files",
1073
0
                                     NULL,
1074
0
                                     G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
1075
0
                                     G_PARAM_STATIC_STRINGS);
1076
1077
        /**
1078
         * SoupCache:cache-type:
1079
         * Whether the cache is private or shared.
1080
         */
1081
0
        properties[PROP_CACHE_TYPE] =
1082
0
                g_param_spec_enum ("cache-type",
1083
0
                                   "Cache type",
1084
0
                                   "Whether the cache is private or shared",
1085
0
                                   SOUP_TYPE_CACHE_TYPE,
1086
0
                                   SOUP_CACHE_SINGLE_USER,
1087
0
                                   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
1088
0
                                   G_PARAM_STATIC_STRINGS);
1089
1090
0
        g_object_class_install_properties (gobject_class, LAST_PROPERTY, properties);
1091
0
}
1092
1093
/**
1094
 * SoupCacheType:
1095
 * @SOUP_CACHE_SINGLE_USER: a single-user cache
1096
 * @SOUP_CACHE_SHARED: a shared cache
1097
 *
1098
 * The type of cache; this affects what kinds of responses will be
1099
 * saved.
1100
 *
1101
 */
1102
1103
/**
1104
 * soup_cache_new:
1105
 * @cache_dir: (nullable): the directory to store the cached data, or %NULL
1106
 *   to use the default one. Note that since the cache isn't safe to access for
1107
 *   multiple processes at once, and the default directory isn't namespaced by
1108
 *   process, clients are strongly discouraged from passing %NULL.
1109
 * @cache_type: the #SoupCacheType of the cache
1110
 *
1111
 * Creates a new #SoupCache.
1112
 *
1113
 * Returns: a new #SoupCache
1114
 *
1115
 */
1116
SoupCache *
1117
soup_cache_new (const char *cache_dir, SoupCacheType cache_type)
1118
0
{
1119
0
  return g_object_new (SOUP_TYPE_CACHE,
1120
0
           "cache-dir", cache_dir,
1121
0
           "cache-type", cache_type,
1122
0
           NULL);
1123
0
}
1124
1125
/**
1126
 * soup_cache_has_response:
1127
 * @cache: a #SoupCache
1128
 * @msg: a #SoupMessage
1129
 *
1130
 * This function calculates whether the @cache object has a proper
1131
 * response for the request @msg given the flags both in the request
1132
 * and the cached reply and the time ellapsed since it was cached.
1133
 *
1134
 * Returns: whether or not the @cache has a valid response for @msg
1135
 *
1136
 */
1137
SoupCacheResponse
1138
soup_cache_has_response (SoupCache *cache, SoupMessage *msg)
1139
0
{
1140
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1141
0
  SoupCacheEntry *entry;
1142
0
  const char *cache_control;
1143
0
  gpointer value;
1144
0
  int max_age, max_stale, min_fresh;
1145
0
  GList *lru_item, *item;
1146
1147
0
        g_mutex_lock (&priv->mutex);
1148
1149
0
  entry = soup_cache_entry_lookup (cache, msg);
1150
1151
  /* 1. The presented Request-URI and that of stored response
1152
   * match
1153
   */
1154
0
  if (!entry) {
1155
0
                g_mutex_unlock (&priv->mutex);
1156
0
    return SOUP_CACHE_RESPONSE_STALE;
1157
0
        }
1158
1159
  /* Increase hit count. Take sorting into account */
1160
0
  entry->hits++;
1161
0
  lru_item = g_list_find (priv->lru_start, entry);
1162
0
  item = lru_item;
1163
0
  while (item->next && lru_compare_func (item->data, item->next->data) > 0)
1164
0
    item = g_list_next (item);
1165
1166
0
  if (item != lru_item) {
1167
0
    priv->lru_start = g_list_remove_link (priv->lru_start, lru_item);
1168
0
    item = g_list_insert_sorted (item, lru_item->data, lru_compare_func);
1169
0
                (void)item; // Silence scan-build warning
1170
0
    g_list_free (lru_item);
1171
0
  }
1172
1173
0
        g_mutex_unlock (&priv->mutex);
1174
1175
0
  if (entry->dirty || entry->being_validated)
1176
0
    return SOUP_CACHE_RESPONSE_STALE;
1177
1178
  /* 2. The request method associated with the stored response
1179
   *  allows it to be used for the presented request
1180
   */
1181
1182
  /* In practice this means we only return our resource for GET,
1183
   * cacheability for other methods is a TODO in the RFC
1184
   * (TODO: although we could return the headers for HEAD
1185
   * probably).
1186
   */
1187
0
  if (soup_message_get_method (msg) != SOUP_METHOD_GET)
1188
0
    return SOUP_CACHE_RESPONSE_STALE;
1189
1190
  /* 3. Selecting request-headers nominated by the stored
1191
   * response (if any) match those presented.
1192
   */
1193
1194
  /* TODO */
1195
1196
  /* 4. The request is a conditional request issued by the client.
1197
   */
1198
0
  if (soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_MODIFIED_SINCE) ||
1199
0
      soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_NONE_MATCH))
1200
0
    return SOUP_CACHE_RESPONSE_STALE;
1201
1202
  /* 5. The presented request and stored response are free from
1203
   * directives that would prevent its use.
1204
   */
1205
1206
0
  max_age = max_stale = min_fresh = -1;
1207
1208
  /* For HTTP 1.0 compatibility. RFC2616 section 14.9.4
1209
   */
1210
0
  if (soup_message_headers_header_contains_common (soup_message_get_request_headers (msg), SOUP_HEADER_PRAGMA, "no-cache"))
1211
0
    return SOUP_CACHE_RESPONSE_STALE;
1212
1213
0
  cache_control = soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_CACHE_CONTROL);
1214
0
  if (cache_control && *cache_control) {
1215
0
    GHashTable *hash = soup_header_parse_param_list (cache_control);
1216
1217
0
    if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) {
1218
0
      soup_header_free_param_list (hash);
1219
0
      return SOUP_CACHE_RESPONSE_STALE;
1220
0
    }
1221
1222
0
    if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) {
1223
0
      soup_header_free_param_list (hash);
1224
0
      return SOUP_CACHE_RESPONSE_STALE;
1225
0
    }
1226
1227
0
    if (g_hash_table_lookup_extended (hash, "max-age", NULL, &value) && value) {
1228
0
      max_age = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32);
1229
      /* Forcing cache revalidaton
1230
       */
1231
0
      if (!max_age) {
1232
0
        soup_header_free_param_list (hash);
1233
0
        return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1234
0
      }
1235
0
    }
1236
1237
    /* max-stale can have no value set, we need to use _extended */
1238
0
    if (g_hash_table_lookup_extended (hash, "max-stale", NULL, &value)) {
1239
0
      if (value)
1240
0
        max_stale = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32);
1241
0
      else
1242
0
        max_stale = G_MAXINT32;
1243
0
    }
1244
1245
0
    value = g_hash_table_lookup (hash, "min-fresh");
1246
0
    if (value)
1247
0
      min_fresh = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32);
1248
1249
0
    soup_header_free_param_list (hash);
1250
1251
0
    if (max_age > 0) {
1252
0
      guint current_age = soup_cache_entry_get_current_age (entry);
1253
1254
      /* If we are over max-age and max-stale is not
1255
         set, do not use the value from the cache
1256
         without validation */
1257
0
      if ((guint) max_age <= current_age && max_stale == -1)
1258
0
        return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1259
0
    }
1260
0
  }
1261
1262
  /* 6. The stored response is either: fresh, allowed to be
1263
   * served stale or succesfully validated
1264
   */
1265
0
  if (!soup_cache_entry_is_fresh_enough (entry, min_fresh)) {
1266
    /* Not fresh, can it be served stale? */
1267
1268
    /* When the must-revalidate directive is present in a
1269
     * response received by a cache, that cache MUST NOT
1270
     * use the entry after it becomes stale
1271
     */
1272
    /* TODO consider also proxy-revalidate & s-maxage */
1273
0
    if (entry->must_revalidate)
1274
0
      return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1275
1276
0
    if (max_stale != -1) {
1277
      /* G_MAXINT32 means we accept any staleness */
1278
0
      if (max_stale == G_MAXINT32)
1279
0
        return SOUP_CACHE_RESPONSE_FRESH;
1280
1281
0
      if ((soup_cache_entry_get_current_age (entry) - entry->freshness_lifetime) <= (guint) max_stale)
1282
0
        return SOUP_CACHE_RESPONSE_FRESH;
1283
0
    }
1284
1285
0
    return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1286
0
  }
1287
1288
0
  return SOUP_CACHE_RESPONSE_FRESH;
1289
0
}
1290
1291
/**
1292
 * soup_cache_get_cacheability:
1293
 * @cache: a #SoupCache
1294
 * @msg: a #SoupMessage
1295
 *
1296
 * Calculates whether the @msg can be cached or not.
1297
 *
1298
 * Returns: a [flags@Cacheability] value indicating whether the @msg can be cached
1299
 *   or not.
1300
 */
1301
SoupCacheability
1302
soup_cache_get_cacheability (SoupCache *cache, SoupMessage *msg)
1303
0
{
1304
0
  g_return_val_if_fail (SOUP_IS_CACHE (cache), SOUP_CACHE_UNCACHEABLE);
1305
0
  g_return_val_if_fail (SOUP_IS_MESSAGE (msg), SOUP_CACHE_UNCACHEABLE);
1306
1307
0
  return SOUP_CACHE_GET_CLASS (cache)->get_cacheability (cache, msg);
1308
0
}
1309
1310
static gboolean
1311
force_flush_timeout (gpointer data)
1312
0
{
1313
0
  gboolean *forced = (gboolean *)data;
1314
0
  *forced = TRUE;
1315
1316
0
  return FALSE;
1317
0
}
1318
1319
/**
1320
 * soup_cache_flush:
1321
 * @cache: a #SoupCache
1322
 *
1323
 * Forces all pending writes in the @cache to be
1324
 * committed to disk.
1325
 *
1326
 * For doing so it will iterate the [struct@GLib.MainContext] associated with
1327
 * @cache's session as long as needed.
1328
 *
1329
 * Contrast with [method@Cache.dump], which writes out the cache index file.
1330
 */
1331
void
1332
soup_cache_flush (SoupCache *cache)
1333
0
{
1334
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1335
0
  GMainContext *async_context;
1336
0
  SoupSession *session;
1337
0
  GSource *timeout;
1338
0
  gboolean forced = FALSE;
1339
1340
0
  g_return_if_fail (SOUP_IS_CACHE (cache));
1341
1342
0
  session = priv->session;
1343
0
  g_return_if_fail (SOUP_IS_SESSION (session));
1344
0
  async_context = g_main_context_get_thread_default ();
1345
1346
  /* We give cache 10 secs to finish */
1347
0
  timeout = soup_add_timeout (async_context, 10000, force_flush_timeout, &forced);
1348
1349
0
  while (!forced && priv->n_pending > 0)
1350
0
    g_main_context_iteration (async_context, FALSE);
1351
1352
0
  if (!forced)
1353
0
    g_source_destroy (timeout);
1354
0
  else
1355
0
    g_warning ("Cache flush finished despite %d pending requests", priv->n_pending);
1356
1357
0
        g_source_unref (timeout);
1358
0
}
1359
1360
typedef void (* SoupCacheForeachFileFunc) (SoupCache *cache, const char *name, gpointer user_data);
1361
1362
static void
1363
soup_cache_foreach_file (SoupCache *cache, SoupCacheForeachFileFunc func, gpointer user_data)
1364
0
{
1365
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1366
0
  GDir *dir;
1367
0
  const char *name;
1368
1369
0
  dir = g_dir_open (priv->cache_dir, 0, NULL);
1370
0
  while ((name = g_dir_read_name (dir))) {
1371
0
    if (g_str_has_prefix (name, "soup."))
1372
0
        continue;
1373
1374
0
    func (cache, name, user_data);
1375
0
  }
1376
0
  g_dir_close (dir);
1377
0
}
1378
1379
static void
1380
clear_cache_item (gpointer data,
1381
      gpointer user_data)
1382
0
{
1383
0
  soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, TRUE);
1384
0
}
1385
1386
static void
1387
delete_cache_file (SoupCache *cache, const char *name, gpointer user_data)
1388
0
{
1389
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1390
0
  gchar *path;
1391
1392
0
  path = g_build_filename (priv->cache_dir, name, NULL);
1393
0
  g_unlink (path);
1394
0
  g_free (path);
1395
0
}
1396
1397
static void
1398
clear_cache_files (SoupCache *cache)
1399
0
{
1400
0
  soup_cache_foreach_file (cache, delete_cache_file, NULL);
1401
0
}
1402
1403
/**
1404
 * soup_cache_clear:
1405
 * @cache: a #SoupCache
1406
 *
1407
 * Will remove all entries in the @cache plus all the cache files.
1408
 *
1409
 * This is not thread safe and must be called only from the thread that created the [class@Cache]
1410
 */
1411
void
1412
soup_cache_clear (SoupCache *cache)
1413
0
{
1414
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1415
0
  GList *entries;
1416
1417
0
  g_return_if_fail (SOUP_IS_CACHE (cache));
1418
0
  g_return_if_fail (priv->cache);
1419
1420
  /* Cannot use g_hash_table_foreach as callbacks must not modify the hash table */
1421
0
  entries = g_hash_table_get_values (priv->cache);
1422
0
  g_list_foreach (entries, clear_cache_item, cache);
1423
0
  g_list_free (entries);
1424
1425
  /* Remove also any file not associated with a cache entry. */
1426
0
  clear_cache_files (cache);
1427
0
}
1428
1429
SoupMessage *
1430
soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original)
1431
0
{
1432
0
        SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1433
0
  SoupMessage *msg;
1434
0
  GUri *uri;
1435
0
  SoupCacheEntry *entry;
1436
0
  const char *last_modified, *etag;
1437
0
  GList *disabled_features, *f;
1438
1439
0
  g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL);
1440
0
  g_return_val_if_fail (SOUP_IS_MESSAGE (original), NULL);
1441
1442
  /* Add the validator entries in the header from the cached data */
1443
0
        g_mutex_lock (&priv->mutex);
1444
0
  entry = soup_cache_entry_lookup (cache, original);
1445
0
        g_mutex_unlock (&priv->mutex);
1446
0
  g_return_val_if_fail (entry, NULL);
1447
1448
0
  last_modified = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_LAST_MODIFIED);
1449
0
  etag = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_ETAG);
1450
1451
0
  if (!last_modified && !etag)
1452
0
    return NULL;
1453
1454
0
  entry->being_validated = TRUE;
1455
1456
  /* Copy the data we need from the original message */
1457
0
  uri = soup_message_get_uri (original);
1458
0
  msg = soup_message_new_from_uri (soup_message_get_method (original), uri);
1459
0
  soup_message_set_flags (msg, soup_message_get_flags (original));
1460
0
  soup_message_disable_feature (msg, SOUP_TYPE_CACHE);
1461
1462
0
  soup_message_headers_foreach (soup_message_get_request_headers (original),
1463
0
              (SoupMessageHeadersForeachFunc)copy_headers,
1464
0
              soup_message_get_request_headers (msg));
1465
1466
0
  disabled_features = soup_message_get_disabled_features (original);
1467
0
  for (f = disabled_features; f; f = f->next)
1468
0
    soup_message_disable_feature (msg, (GType) GPOINTER_TO_SIZE (f->data));
1469
0
  g_list_free (disabled_features);
1470
1471
0
  if (last_modified)
1472
0
    soup_message_headers_append_common (soup_message_get_request_headers (msg),
1473
0
                                                    SOUP_HEADER_IF_MODIFIED_SINCE,
1474
0
                                                    last_modified);
1475
0
  if (etag)
1476
0
    soup_message_headers_append_common (soup_message_get_request_headers (msg),
1477
0
                                                    SOUP_HEADER_IF_NONE_MATCH,
1478
0
                                                    etag);
1479
1480
0
  return msg;
1481
0
}
1482
1483
void
1484
soup_cache_cancel_conditional_request (SoupCache   *cache,
1485
               SoupMessage *msg)
1486
0
{
1487
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1488
0
  SoupCacheEntry *entry;
1489
1490
0
        g_mutex_lock (&priv->mutex);
1491
0
  entry = soup_cache_entry_lookup (cache, msg);
1492
0
        g_mutex_unlock (&priv->mutex);
1493
0
  if (entry)
1494
0
    entry->being_validated = FALSE;
1495
1496
0
  soup_session_cancel_message (priv->session, msg);
1497
0
}
1498
1499
void
1500
soup_cache_update_from_conditional_request (SoupCache   *cache,
1501
              SoupMessage *msg)
1502
0
{
1503
0
        SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1504
0
  SoupCacheEntry *entry;
1505
1506
0
        g_mutex_lock (&priv->mutex);
1507
0
        entry = soup_cache_entry_lookup (cache, msg);
1508
0
        g_mutex_unlock (&priv->mutex);
1509
0
  if (!entry)
1510
0
    return;
1511
1512
0
  entry->being_validated = FALSE;
1513
1514
0
  if (soup_message_get_status (msg) == SOUP_STATUS_NOT_MODIFIED) {
1515
0
    soup_message_headers_foreach (soup_message_get_response_headers (msg),
1516
0
                (SoupMessageHeadersForeachFunc) remove_headers,
1517
0
                entry->headers);
1518
0
    copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers);
1519
1520
0
    soup_cache_entry_set_freshness (entry, msg, cache);
1521
0
  }
1522
0
}
1523
1524
static void
1525
pack_entry (gpointer data,
1526
      gpointer user_data)
1527
0
{
1528
0
  SoupCacheEntry *entry = (SoupCacheEntry *) data;
1529
0
  SoupMessageHeadersIter iter;
1530
0
  const char *header_key, *header_value;
1531
0
  GVariantBuilder *entries_builder = (GVariantBuilder *)user_data;
1532
1533
  /* Do not store non-consolidated entries */
1534
0
  if (entry->dirty || !entry->key)
1535
0
    return;
1536
1537
0
  g_variant_builder_open (entries_builder, G_VARIANT_TYPE (SOUP_CACHE_PHEADERS_FORMAT));
1538
0
  g_variant_builder_add (entries_builder, "s", entry->uri);
1539
0
  g_variant_builder_add (entries_builder, "b", entry->must_revalidate);
1540
0
  g_variant_builder_add (entries_builder, "u", entry->freshness_lifetime);
1541
0
  g_variant_builder_add (entries_builder, "u", entry->corrected_initial_age);
1542
0
  g_variant_builder_add (entries_builder, "u", entry->response_time);
1543
0
  g_variant_builder_add (entries_builder, "u", entry->hits);
1544
0
  g_variant_builder_add (entries_builder, "u", entry->length);
1545
0
  g_variant_builder_add (entries_builder, "q", entry->status_code);
1546
1547
  /* Pack headers */
1548
0
  g_variant_builder_open (entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_HEADERS_FORMAT));
1549
0
  soup_message_headers_iter_init (&iter, entry->headers);
1550
0
  while (soup_message_headers_iter_next (&iter, &header_key, &header_value)) {
1551
0
    if (g_utf8_validate (header_value, -1, NULL))
1552
0
      g_variant_builder_add (entries_builder, SOUP_CACHE_HEADERS_FORMAT,
1553
0
                 header_key, header_value);
1554
0
  }
1555
0
  g_variant_builder_close (entries_builder); /* "a" SOUP_CACHE_HEADERS_FORMAT */
1556
0
  g_variant_builder_close (entries_builder); /* SOUP_CACHE_PHEADERS_FORMAT */
1557
0
}
1558
1559
/**
1560
 * soup_cache_dump:
1561
 * @cache: a #SoupCache
1562
 *
1563
 * Synchronously writes the cache index out to disk.
1564
 *
1565
 * Contrast with [method@Cache.flush], which writes pending cache *entries* to
1566
 * disk.
1567
 *
1568
 * You must call this before exiting if you want your cache data to
1569
 * persist between sessions.
1570
 *
1571
 * This is not thread safe and must be called only from the thread that created the [class@Cache]
1572
 */
1573
void
1574
soup_cache_dump (SoupCache *cache)
1575
0
{
1576
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1577
0
  char *filename;
1578
0
  GVariantBuilder entries_builder;
1579
0
  GVariant *cache_variant;
1580
1581
0
  if (!g_list_length (priv->lru_start))
1582
0
    return;
1583
1584
  /* Create the builder and iterate over all entries */
1585
0
  g_variant_builder_init (&entries_builder, G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT));
1586
0
  g_variant_builder_add (&entries_builder, "q", SOUP_CACHE_CURRENT_VERSION);
1587
0
  g_variant_builder_open (&entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_PHEADERS_FORMAT));
1588
0
  g_list_foreach (priv->lru_start, pack_entry, &entries_builder);
1589
0
  g_variant_builder_close (&entries_builder);
1590
1591
  /* Serialize and dump */
1592
0
  cache_variant = g_variant_builder_end (&entries_builder);
1593
0
  g_variant_ref_sink (cache_variant);
1594
0
  filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL);
1595
0
  g_file_set_contents (filename, (const char *) g_variant_get_data (cache_variant),
1596
0
           g_variant_get_size (cache_variant), NULL);
1597
0
  g_free (filename);
1598
0
  g_variant_unref (cache_variant);
1599
0
}
1600
1601
static inline guint32
1602
get_key_from_cache_filename (const char *name)
1603
0
{
1604
0
  guint64 key;
1605
1606
0
  key = g_ascii_strtoull (name, NULL, 10);
1607
0
  return key ? (guint32)key : 0;
1608
0
}
1609
1610
static void
1611
insert_cache_file (SoupCache *cache, const char *name, GHashTable *leaked_entries)
1612
0
{
1613
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1614
0
  gchar *path;
1615
1616
0
  path = g_build_filename (priv->cache_dir, name, NULL);
1617
0
  if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) {
1618
0
    guint32 key = get_key_from_cache_filename (name);
1619
1620
0
    if (key) {
1621
0
      g_hash_table_insert (leaked_entries, GUINT_TO_POINTER (key), path);
1622
0
      return;
1623
0
    }
1624
0
  }
1625
0
  g_free (path);
1626
0
}
1627
1628
/**
1629
 * soup_cache_load:
1630
 * @cache: a #SoupCache
1631
 *
1632
 * Loads the contents of @cache's index into memory.
1633
 *
1634
 * This is not thread safe and must be called only from the thread that created the [class@Cache]
1635
 */
1636
void
1637
soup_cache_load (SoupCache *cache)
1638
0
{
1639
0
  gboolean must_revalidate;
1640
0
  guint32 freshness_lifetime, hits;
1641
0
  guint32 corrected_initial_age, response_time;
1642
0
  char *url, *filename = NULL, *contents = NULL;
1643
0
  GVariant *cache_variant;
1644
0
  GVariantIter *entries_iter = NULL, *headers_iter = NULL;
1645
0
  gsize length;
1646
0
  SoupCacheEntry *entry;
1647
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1648
0
  guint16 version, status_code;
1649
0
  GHashTable *leaked_entries = NULL;
1650
0
  GHashTableIter iter;
1651
0
  gpointer value;
1652
1653
0
  filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL);
1654
0
  if (!g_file_get_contents (filename, &contents, &length, NULL)) {
1655
0
    g_free (filename);
1656
0
    g_free (contents);
1657
0
    clear_cache_files (cache);
1658
0
    return;
1659
0
  }
1660
0
  g_free (filename);
1661
1662
0
  cache_variant = g_variant_new_from_data (G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT),
1663
0
             (const char *) contents, length, FALSE, g_free, contents);
1664
0
  g_variant_get (cache_variant, SOUP_CACHE_ENTRIES_FORMAT, &version, &entries_iter);
1665
0
  if (version != SOUP_CACHE_CURRENT_VERSION) {
1666
0
    g_variant_iter_free (entries_iter);
1667
0
    g_variant_unref (cache_variant);
1668
0
    clear_cache_files (cache);
1669
0
    return;
1670
0
  }
1671
1672
0
  leaked_entries = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
1673
0
  soup_cache_foreach_file (cache, (SoupCacheForeachFileFunc)insert_cache_file, leaked_entries);
1674
1675
0
  while (g_variant_iter_loop (entries_iter, SOUP_CACHE_PHEADERS_FORMAT,
1676
0
            &url, &must_revalidate, &freshness_lifetime, &corrected_initial_age,
1677
0
            &response_time, &hits, &length, &status_code,
1678
0
            &headers_iter)) {
1679
0
    const char *header_key, *header_value;
1680
0
    SoupMessageHeaders *headers;
1681
0
    SoupMessageHeadersIter soup_headers_iter;
1682
1683
    /* SoupMessage Headers */
1684
0
    headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
1685
0
    while (g_variant_iter_loop (headers_iter, SOUP_CACHE_HEADERS_FORMAT, &header_key, &header_value))
1686
0
      if (*header_key && *header_value)
1687
0
        soup_message_headers_append (headers, header_key, header_value);
1688
1689
    /* Check that we have headers */
1690
0
    soup_message_headers_iter_init (&soup_headers_iter, headers);
1691
0
    if (!soup_message_headers_iter_next (&soup_headers_iter, &header_key, &header_value)) {
1692
0
      soup_message_headers_unref (headers);
1693
0
      continue;
1694
0
    }
1695
1696
    /* Insert in cache */
1697
0
    entry = g_slice_new0 (SoupCacheEntry);
1698
0
    entry->uri = g_strdup (url);
1699
0
    entry->must_revalidate = must_revalidate;
1700
0
    entry->freshness_lifetime = freshness_lifetime;
1701
0
    entry->corrected_initial_age = corrected_initial_age;
1702
0
    entry->response_time = response_time;
1703
0
    entry->hits = hits;
1704
0
    entry->length = length;
1705
0
    entry->headers = headers;
1706
0
    entry->status_code = status_code;
1707
1708
0
    if (!soup_cache_entry_insert (cache, entry, FALSE))
1709
0
      soup_cache_entry_free (entry);
1710
0
    else
1711
0
      g_hash_table_remove (leaked_entries, GUINT_TO_POINTER (entry->key));
1712
0
  }
1713
1714
  /* Remove the leaked files */
1715
0
  g_hash_table_iter_init (&iter, leaked_entries);
1716
0
  while (g_hash_table_iter_next (&iter, NULL, &value))
1717
0
    g_unlink ((char *)value);
1718
0
  g_hash_table_destroy (leaked_entries);
1719
1720
0
  priv->lru_start = g_list_reverse (priv->lru_start);
1721
1722
  /* frees */
1723
0
  g_variant_iter_free (entries_iter);
1724
0
  g_variant_unref (cache_variant);
1725
0
}
1726
1727
/**
1728
 * soup_cache_set_max_size:
1729
 * @cache: a #SoupCache
1730
 * @max_size: the maximum size of the cache, in bytes
1731
 *
1732
 * Sets the maximum size of the cache.
1733
 */
1734
void
1735
soup_cache_set_max_size (SoupCache *cache,
1736
       guint      max_size)
1737
0
{
1738
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1739
0
  priv->max_size = max_size;
1740
0
  priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE;
1741
0
}
1742
1743
/**
1744
 * soup_cache_get_max_size:
1745
 * @cache: a #SoupCache
1746
 *
1747
 * Gets the maximum size of the cache.
1748
 *
1749
 * Returns: the maximum size of the cache, in bytes.
1750
 */
1751
guint
1752
soup_cache_get_max_size (SoupCache *cache)
1753
0
{
1754
0
  SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
1755
0
  return priv->max_size;
1756
0
}