Coverage Report

Created: 2025-12-27 06:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libsoup/libsoup/soup-date-utils.c
Line
Count
Source
1
/*
2
 * Copyright (C) 2020 Igalia, S.L.
3
 *
4
 * This library is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU Library General Public
6
 * License as published by the Free Software Foundation; either
7
 * version 2 of the License, or (at your option) any later version.
8
 *
9
 * This library is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12
 * Library General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Library General Public License
15
 * along with this library; see the file COPYING.LIB.  If not, write to
16
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17
 * Boston, MA 02110-1301, USA.
18
 */
19
20
#ifdef HAVE_CONFIG_H
21
#include <config.h>
22
#endif
23
24
#include <stdlib.h>
25
26
#include "soup-date-utils.h"
27
#include "soup-date-utils-private.h"
28
29
/**
30
 * soup_date_time_is_past:
31
 * @date: a #GDateTime
32
 *
33
 * Determines if @date is in the past.
34
 *
35
 * Returns: %TRUE if @date is in the past
36
 */
37
gboolean
38
soup_date_time_is_past (GDateTime *date)
39
0
{
40
0
        g_return_val_if_fail (date != NULL, TRUE);
41
42
  /* optimization */
43
0
  if (g_date_time_get_year (date) < 2025)
44
0
    return TRUE;
45
46
0
  return g_date_time_to_unix (date) < time (NULL);
47
0
}
48
49
/**
50
 * SoupDateFormat:
51
 * @SOUP_DATE_HTTP: RFC 1123 format, used by the HTTP "Date" header. Eg
52
 *   "Sun, 06 Nov 1994 08:49:37 GMT".
53
 * @SOUP_DATE_COOKIE: The format for the "Expires" timestamp in the
54
 *   Netscape cookie specification. Eg, "Sun, 06-Nov-1994 08:49:37 GMT".
55
 *
56
 * Date formats that [func@date_time_to_string] can use.
57
 *
58
 * @SOUP_DATE_HTTP and @SOUP_DATE_COOKIE always coerce the time to
59
 * UTC.
60
 *
61
 * This enum may be extended with more values in future releases.
62
 **/
63
64
/* Do not internationalize */
65
static const char *const months[] = {
66
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
67
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
68
};
69
70
/* Do not internationalize */
71
static const char *const days[] = {
72
  "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"
73
};
74
75
/**
76
 * soup_date_time_to_string:
77
 * @date: a #GDateTime
78
 * @format: the format to generate the date in
79
 *
80
 * Converts @date to a string in the format described by @format.
81
 *
82
 * Returns: (transfer full): @date as a string or %NULL
83
 **/
84
char *
85
soup_date_time_to_string (GDateTime      *date,
86
                          SoupDateFormat  format)
87
0
{
88
0
  g_return_val_if_fail (date != NULL, NULL);
89
90
0
  if (format == SOUP_DATE_HTTP || format == SOUP_DATE_COOKIE) {
91
    /* HTTP and COOKIE formats require UTC timestamp, so coerce
92
     * @date if it's non-UTC.
93
     */
94
0
    GDateTime *utcdate = g_date_time_to_utc (date);
95
0
                char *date_format;
96
0
                char *formatted_date;
97
98
0
                if (!utcdate)
99
0
                        return NULL;
100
101
                // We insert days/months ourselves to avoid locale specific formatting
102
0
                if (format == SOUP_DATE_HTTP) {
103
      /* "Sun, 06 Nov 1994 08:49:37 GMT" */
104
0
                        date_format = g_strdup_printf ("%s, %%d %s %%Y %%T GMT",
105
0
                                                       days[g_date_time_get_day_of_week (utcdate) - 1],
106
0
                                                       months[g_date_time_get_month (utcdate) - 1]);
107
0
                } else {
108
      /* "Sun, 06-Nov-1994 08:49:37 GMT" */
109
0
                        date_format = g_strdup_printf ("%s, %%d-%s-%%Y %%T GMT",
110
0
                                                       days[g_date_time_get_day_of_week (utcdate) - 1],
111
0
                                                       months[g_date_time_get_month (utcdate) - 1]);
112
0
    }
113
114
0
                formatted_date = g_date_time_format (utcdate, (const char*)date_format);
115
0
                g_date_time_unref (utcdate);
116
0
                g_free (date_format);
117
0
                return formatted_date;
118
0
  }
119
120
0
        g_return_val_if_reached (NULL);
121
0
}
122
123
static inline gboolean
124
parse_day (int *day, const char **date_string)
125
1.55k
{
126
1.55k
  char *end;
127
128
1.55k
  *day = strtoul (*date_string, &end, 10);
129
1.55k
  if (end == (char *)*date_string)
130
73
    return FALSE;
131
132
3.06k
  while (*end == ' ' || *end == '-')
133
1.58k
    end++;
134
1.48k
  *date_string = end;
135
1.48k
  return *day >= 1 && *day <= 31;
136
1.55k
}
137
138
static inline gboolean
139
parse_month (int *month, const char **date_string)
140
1.44k
{
141
1.44k
  int i;
142
143
6.09k
  for (i = 0; i < G_N_ELEMENTS (months); i++) {
144
6.04k
    if (!g_ascii_strncasecmp (*date_string, months[i], 3)) {
145
1.40k
      *month = i + 1;
146
1.40k
      *date_string += 3;
147
6.94k
      while (**date_string == ' ' || **date_string == '-')
148
5.54k
        (*date_string)++;
149
1.40k
      return TRUE;
150
1.40k
    }
151
6.04k
  }
152
45
  return FALSE;
153
1.44k
}
154
155
static inline gboolean
156
parse_year (int *year, const char **date_string)
157
1.00k
{
158
1.00k
  char *end;
159
160
1.00k
  *year = strtoul (*date_string, &end, 10);
161
1.00k
  if (end == (char *)*date_string)
162
60
    return FALSE;
163
164
944
  if (end == (char *)*date_string + 2) {
165
50
    if (*year < 70)
166
43
      *year += 2000;
167
7
    else
168
7
      *year += 1900;
169
894
  } else if (end == (char *)*date_string + 3)
170
12
    *year += 1900;
171
172
2.23k
  while (*end == ' ' || *end == '-')
173
1.28k
    end++;
174
944
  *date_string = end;
175
944
  return *year > 0 && *year < 9999;
176
1.00k
}
177
178
static inline gboolean
179
parse_time (int *hour, int *minute, int *second, const char **date_string)
180
1.18k
{
181
1.18k
  char *p, *end;
182
183
1.18k
  *hour = strtoul (*date_string, &end, 10);
184
1.18k
  if (end == (char *)*date_string || *end++ != ':')
185
128
    return FALSE;
186
1.06k
  p = end;
187
1.06k
  *minute = strtoul (p, &end, 10);
188
1.06k
  if (end == p || *end++ != ':')
189
21
    return FALSE;
190
1.04k
  p = end;
191
1.04k
  *second = strtoul (p, &end, 10);
192
1.04k
  if (end == p)
193
2
    return FALSE;
194
1.03k
  p = end;
195
196
1.46k
  while (*p == ' ')
197
428
    p++;
198
1.03k
  *date_string = p;
199
1.03k
  return *hour >= 0 && *hour < 24 && *minute >= 0 && *minute < 60 && *second >= 0 && *second < 60;
200
1.04k
}
201
202
static inline gboolean
203
parse_timezone (GTimeZone **timezone, const char **date_string)
204
480
{
205
480
        gint32 offset_minutes;
206
480
        gboolean utc;
207
208
480
  if (!**date_string) {
209
94
                utc = FALSE;
210
94
    offset_minutes = 0;
211
386
  } else if (**date_string == '+' || **date_string == '-') {
212
285
    gulong val;
213
285
    int sign = (**date_string == '+') ? 1 : -1;
214
285
    val = strtoul (*date_string + 1, (char **)date_string, 10);
215
285
    if (val > 9999)
216
98
      return FALSE;
217
187
    if (**date_string == ':') {
218
143
      gulong val2 = strtoul (*date_string + 1, (char **)date_string, 10);
219
143
      if (val > 99 || val2 > 99)
220
117
        return FALSE;
221
26
      val = 60 * val + val2;
222
26
    } else
223
44
      val =  60 * (val / 100) + (val % 100);
224
70
    offset_minutes = sign * val;
225
70
    utc = (sign == -1) && !val;
226
101
  } else if (**date_string == 'Z') {
227
1
    offset_minutes = 0;
228
1
    utc = TRUE;
229
1
    (*date_string)++;
230
100
  } else if (!strcmp (*date_string, "GMT") ||
231
97
       !strcmp (*date_string, "UTC")) {
232
4
    offset_minutes = 0;
233
4
    utc = TRUE;
234
4
    (*date_string) += 3;
235
96
  } else if (strchr ("ECMP", **date_string) &&
236
24
       ((*date_string)[1] == 'D' || (*date_string)[1] == 'S') &&
237
6
       (*date_string)[2] == 'T') {
238
2
    offset_minutes = -60 * (5 * strcspn ("ECMP", *date_string));
239
2
    if ((*date_string)[1] == 'D')
240
1
      offset_minutes += 60;
241
2
                utc = FALSE;
242
2
  } else
243
94
    return FALSE;
244
245
171
        if (utc)
246
12
                *timezone = g_time_zone_new_utc ();
247
159
        else
248
159
                *timezone = g_time_zone_new_offset (offset_minutes * 60);
249
171
  return TRUE;
250
480
}
251
252
static GDateTime *
253
parse_textual_date (const char *date_string)
254
1.56k
{
255
1.56k
        int month, day, year, hour, minute, second;
256
1.56k
        GTimeZone *tz = NULL;
257
1.56k
        GDateTime *date;
258
259
  /* If it starts with a word, it must be a weekday, which we skip */
260
1.56k
  if (g_ascii_isalpha (*date_string)) {
261
1.49k
    while (g_ascii_isalpha (*date_string))
262
855
      date_string++;
263
643
    if (*date_string == ',')
264
238
      date_string++;
265
1.17k
    while (g_ascii_isspace (*date_string))
266
527
      date_string++;
267
643
  }
268
269
  /* If there's now another word, this must be an asctime-date */
270
1.56k
  if (g_ascii_isalpha (*date_string)) {
271
    /* (Sun) Nov  6 08:49:37 1994 */
272
568
    if (!parse_month (&month, &date_string) ||
273
558
        !parse_day (&day, &date_string) ||
274
444
        !parse_time (&hour, &minute, &second, &date_string) ||
275
162
        !parse_year (&year, &date_string) ||
276
68
        !g_date_valid_dmy (day, month, year))
277
501
      return NULL;
278
279
    /* There shouldn't be a timezone, but check anyway */
280
67
    parse_timezone (&tz, &date_string);
281
998
  } else {
282
    /* Non-asctime date, so some variation of
283
     * (Sun,) 06 Nov 1994 08:49:37 GMT
284
     */
285
998
    if (!parse_day (&day, &date_string) ||
286
877
        !parse_month (&month, &date_string) ||
287
842
        !parse_year (&year, &date_string) ||
288
745
        !parse_time (&hour, &minute, &second, &date_string) ||
289
414
        !g_date_valid_dmy (day, month, year))
290
585
      return NULL;
291
292
    /* This time there *should* be a timezone, but we
293
     * survive if there isn't.
294
     */
295
413
    parse_timezone (&tz, &date_string);
296
413
  }
297
298
480
        if (!tz)
299
309
                tz = g_time_zone_new_utc ();
300
301
480
        date = g_date_time_new (tz, year, month, day, hour, minute, second);
302
480
        g_time_zone_unref (tz);
303
304
480
        return date;
305
1.56k
}
306
307
/**
308
 * soup_date_time_new_from_http_string:
309
 * @date_string: The date as a string
310
 *
311
 * Parses @date_string and tries to extract a date from it.
312
 *
313
 * This recognizes all of the "HTTP-date" formats from RFC 2616, RFC 2822 dates,
314
 * and reasonable approximations thereof. (Eg, it is lenient about whitespace,
315
 * leading "0"s, etc.)
316
 *
317
 * Returns: (nullable): a new #GDateTime, or %NULL if @date_string
318
 *   could not be parsed.
319
 **/
320
GDateTime *
321
soup_date_time_new_from_http_string (const char *date_string)
322
1.56k
{
323
1.56k
        g_return_val_if_fail (date_string != NULL, NULL);
324
325
1.78k
  while (g_ascii_isspace (*date_string))
326
220
    date_string++;
327
328
        /* If it starts with a digit, it's either an ISO 8601 date, or
329
         * an RFC2822 date without the optional weekday; in the later
330
         * case, there will be a month name later on, so look for one
331
         * of the month-start letters.
332
         * Previous versions of this library supported parsing iso8601 strings
333
         * however g_date_time_new_from_iso8601() should be used now. Just
334
         * catch those in case for testing.
335
         */
336
1.56k
  if (G_UNLIKELY (g_ascii_isdigit (*date_string) && !strpbrk (date_string, "JFMASOND"))) {
337
1
                g_debug ("Unsupported format passed to soup_date_time_new_from_http_string(): %s", date_string);
338
1
                return NULL;
339
1
        }
340
341
1.56k
  return parse_textual_date (date_string);
342
1.56k
}