Coverage Report

Created: 2026-09-14 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib/iso8601-date.c
Line
Count
Source
1
/* Copyright (c) Dovecot authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "utc-offset.h"
5
#include "utc-mktime.h"
6
#include "iso8601-date.h"
7
8
#include <ctype.h>
9
10
/* RFC3339/ISO8601 date-time syntax
11
12
   date-fullyear   = 4DIGIT
13
   date-month      = 2DIGIT  ; 01-12
14
   date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
15
                             ; month/year
16
   time-hour       = 2DIGIT  ; 00-23
17
   time-minute     = 2DIGIT  ; 00-59
18
   time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second
19
                             ; rules
20
   time-secfrac    = "." 1*DIGIT
21
   time-numoffset  = ("+" / "-") time-hour ":" time-minute
22
   time-offset     = "Z" / time-numoffset
23
24
   partial-time    = time-hour ":" time-minute ":" time-second [time-secfrac]
25
   full-date       = date-fullyear "-" date-month "-" date-mday
26
   full-time       = partial-time time-offset
27
28
   date-time       = full-date "T" full-time
29
 */
30
31
struct iso8601_date_parser {
32
  const unsigned char *cur, *end;
33
34
  struct tm tm;
35
  int timezone_offset;
36
};
37
38
static inline int
39
iso8601_date_parse_number(struct iso8601_date_parser *parser,
40
        int digits, int *number_r)
41
0
{
42
0
  int i;
43
44
0
  if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
45
0
    return 0;
46
47
0
  *number_r = parser->cur[0] - '0';
48
0
  parser->cur++;
49
50
0
  for (i=0; i < digits-1; i++) {
51
0
    if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
52
0
      return -1;
53
0
    *number_r = ((*number_r) * 10) + parser->cur[0] - '0';
54
0
    parser->cur++;
55
0
  }
56
0
  return 1;
57
0
}
58
59
static int
60
iso8601_date_parse_secfrac(struct iso8601_date_parser *parser)
61
0
{
62
  /* time-secfrac    = "." 1*DIGIT
63
64
     NOTE: Currently not applied anywhere, so fraction is just skipped.
65
  */
66
67
  /* "." */
68
0
  if (parser->cur >= parser->end || parser->cur[0] != '.')
69
0
    return 0;
70
0
  parser->cur++;
71
72
  /* 1DIGIT */
73
0
  if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
74
0
    return -1;
75
0
  parser->cur++;
76
77
  /* *DIGIT */
78
0
  while (parser->cur < parser->end && i_isdigit(parser->cur[0]))
79
0
    parser->cur++;
80
0
  return 1;
81
0
}
82
83
static int is08601_date_parse_time_offset(struct iso8601_date_parser *parser)
84
0
{
85
0
  int tz_sign = 1, tz_hour = 0, tz_min = 0;
86
87
  /* time-offset     = "Z" / time-numoffset
88
     time-numoffset  = ("+" / "-") time-hour ":" time-minute
89
     time-hour       = 2DIGIT  ; 00-23
90
     time-minute     = 2DIGIT  ; 00-59
91
   */
92
93
0
  if (parser->cur >= parser->end)
94
0
    return 0;
95
96
  /* time-offset = "Z" / time-numoffset */
97
0
  switch (parser->cur[0]) {
98
0
  case '-':
99
0
    tz_sign = -1;
100
    /* fall through */
101
0
  case '+':
102
0
    parser->cur++;
103
104
    /* time-hour = 2DIGIT */
105
0
    if (iso8601_date_parse_number(parser, 2, &tz_hour) <= 0)
106
0
      return -1;
107
0
    if (tz_hour > 23)
108
0
      return -1;
109
110
    /* ":" */
111
0
    if (parser->cur >= parser->end || parser->cur[0] != ':')
112
0
      return -1;
113
0
    parser->cur++;
114
115
    /* time-minute = 2DIGIT */
116
0
    if (iso8601_date_parse_number(parser, 2, &tz_min) <= 0)
117
0
      return -1;
118
0
    if (tz_min > 59)
119
0
      return -1;
120
0
    break;
121
0
  case 'Z':
122
0
  case 'z':
123
0
    parser->cur++;
124
0
    break;
125
0
  default:
126
0
    return -1;
127
0
  }
128
129
0
  parser->timezone_offset = tz_sign*(tz_hour*60 + tz_min);
130
0
  return 1;
131
0
}
132
133
static int is08601_date_parse_full_time(struct iso8601_date_parser *parser)
134
0
{
135
  /* full-time       = partial-time time-offset
136
     partial-time    = time-hour ":" time-minute ":" time-second [time-secfrac]
137
     time-hour       = 2DIGIT  ; 00-23
138
     time-minute     = 2DIGIT  ; 00-59
139
     time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second
140
                               ; rules
141
   */
142
143
  /* time-hour = 2DIGIT */
144
0
  if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_hour) <= 0)
145
0
    return -1;
146
147
  /* ":" */
148
0
  if (parser->cur >= parser->end || parser->cur[0] != ':')
149
0
    return -1;
150
0
  parser->cur++;
151
152
  /* time-minute = 2DIGIT */
153
0
  if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_min) <= 0)
154
0
    return -1;
155
156
  /* ":" */
157
0
  if (parser->cur >= parser->end || parser->cur[0] != ':')
158
0
    return -1;
159
0
  parser->cur++;
160
161
  /* time-second = 2DIGIT */
162
0
  if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_sec) <= 0)
163
0
    return -1;
164
165
  /* [time-secfrac] */
166
0
  if (iso8601_date_parse_secfrac(parser) < 0)
167
0
    return -1;
168
169
  /* time-offset */
170
0
  if (is08601_date_parse_time_offset(parser) <= 0)
171
0
    return -1;
172
0
  return 1;
173
0
}
174
175
static int is08601_date_parse_full_date(struct iso8601_date_parser *parser)
176
0
{
177
  /* full-date       = date-fullyear "-" date-month "-" date-mday
178
     date-fullyear   = 4DIGIT
179
     date-month      = 2DIGIT  ; 01-12
180
     date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
181
                               ; month/year
182
   */
183
184
  /* date-fullyear = 4DIGIT */
185
0
  if (iso8601_date_parse_number(parser, 4, &parser->tm.tm_year) <= 0)
186
0
    return -1;
187
0
  if (parser->tm.tm_year < 1900)
188
0
    return -1;
189
0
  parser->tm.tm_year -= 1900;
190
191
  /* "-" */
192
0
  if (parser->cur >= parser->end || parser->cur[0] != '-')
193
0
    return -1;
194
0
  parser->cur++;
195
196
  /* date-month = 2DIGIT */
197
0
  if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_mon) <= 0)
198
0
    return -1;
199
0
  parser->tm.tm_mon -= 1;
200
201
  /* "-" */
202
0
  if (parser->cur >= parser->end || parser->cur[0] != '-')
203
0
    return -1;
204
0
  parser->cur++;
205
206
  /* time-second = 2DIGIT */
207
0
  if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_mday) <= 0)
208
0
    return -1;
209
0
  return 1;
210
0
}
211
212
static int iso8601_date_parse_date_time(struct iso8601_date_parser *parser)
213
0
{
214
  /* date-time       = full-date "T" full-time */
215
216
  /* full-date */
217
0
  if (is08601_date_parse_full_date(parser) <= 0)
218
0
    return -1;
219
220
  /* "T" */
221
0
  if (parser->cur >= parser->end ||
222
0
      (parser->cur[0] != 'T' && parser->cur[0] != 't'))
223
0
    return -1;
224
0
  parser->cur++;
225
226
  /* full-time */
227
0
  if (is08601_date_parse_full_time(parser) <= 0)
228
0
    return -1;
229
230
0
  if (parser->cur != parser->end)
231
0
    return -1;
232
0
  return 1;
233
0
}
234
235
static bool
236
iso8601_date_do_parse(const unsigned char *data, size_t size, struct tm *tm_r,
237
          time_t *timestamp_r, int *timezone_offset_r)
238
0
{
239
0
  struct iso8601_date_parser parser;
240
0
  time_t timestamp;
241
242
0
  i_zero(&parser);
243
0
  parser.cur = data;
244
0
  parser.end = data + size;
245
246
0
  if (iso8601_date_parse_date_time(&parser) <= 0)
247
0
    return FALSE;
248
249
0
  parser.tm.tm_isdst = -1;
250
0
  timestamp = utc_mktime(&parser.tm);
251
0
  if (timestamp == (time_t)-1)
252
0
    return FALSE;
253
254
0
  *timezone_offset_r = parser.timezone_offset;
255
0
  *tm_r = parser.tm;
256
0
  *timestamp_r = timestamp - parser.timezone_offset * 60;
257
0
  return TRUE;
258
0
}
259
260
bool iso8601_date_parse(const unsigned char *data, size_t size,
261
      time_t *timestamp_r, int *timezone_offset_r)
262
0
{
263
0
  struct tm tm;
264
265
0
  return iso8601_date_do_parse(data, size, &tm,
266
0
             timestamp_r, timezone_offset_r);
267
0
}
268
269
bool iso8601_date_parse_tm(const unsigned char *data, size_t size,
270
         struct tm *tm_r, int *timezone_offset_r)
271
0
{
272
0
  time_t timestamp;
273
274
0
  return iso8601_date_do_parse(data, size, tm_r,
275
0
             &timestamp, timezone_offset_r);
276
0
}
277
278
const char *iso8601_date_create_tm(struct tm *tm, int timezone_offset)
279
0
{
280
0
  const char *time_offset;
281
282
0
  if (timezone_offset == INT_MAX)
283
0
    time_offset = "Z";
284
0
  else {
285
0
    char sign = '+';
286
0
    if (timezone_offset < 0) {
287
0
      timezone_offset = -timezone_offset;
288
0
      sign = '-';
289
0
    }
290
0
    time_offset = t_strdup_printf("%c%02d:%02d", sign,
291
0
                timezone_offset / 60,
292
0
                timezone_offset % 60);
293
0
  }
294
295
0
  return t_strdup_printf("%04d-%02d-%02dT%02d:%02d:%02d%s",
296
0
      tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday,
297
0
      tm->tm_hour, tm->tm_min, tm->tm_sec, time_offset);
298
0
}
299
300
const char *iso8601_date_create(time_t timestamp)
301
0
{
302
0
  struct tm *tm;
303
0
  int timezone_offset;
304
305
0
  tm = localtime(&timestamp);
306
0
  timezone_offset = utc_offset(tm, timestamp);
307
308
0
  return iso8601_date_create_tm(tm, timezone_offset);
309
0
}