Coverage Report

Created: 2026-01-10 06:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/S2OPC/src/Common/helpers/sopc_date_time.c
Line
Count
Source
1
/*
2
 * Licensed to Systerel under one or more contributor license
3
 * agreements. See the NOTICE file distributed with this work
4
 * for additional information regarding copyright ownership.
5
 * Systerel licenses this file to you under the Apache
6
 * License, Version 2.0 (the "License"); you may not use this
7
 * file except in compliance with the License. You may obtain
8
 * a copy of the License at
9
 *
10
 *   http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing,
13
 * software distributed under the License is distributed on an
14
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
 * KIND, either express or implied.  See the License for the
16
 * specific language governing permissions and limitations
17
 * under the License.
18
 */
19
20
#include <inttypes.h>
21
#include <limits.h>
22
#include <stdint.h>
23
#include <stdio.h>
24
#include <string.h>
25
26
#include "sopc_date_time.h"
27
28
#include "sopc_assert.h"
29
#include "sopc_helper_string.h"
30
#include "sopc_mem_alloc.h"
31
32
static const SOPC_DateTime SOPC_SECONDS_BETWEEN_EPOCHS = 11644473600;
33
static const SOPC_DateTime SOPC_SECOND_TO_100_NANOSECONDS = 10000000; // 10^7
34
35
char* SOPC_Time_GetString(SOPC_DateTime time, bool local, bool compact)
36
0
{
37
0
    static const size_t buf_size = 24;
38
39
0
    if (time == 0)
40
0
    {
41
0
        return NULL;
42
0
    }
43
44
0
    time_t seconds = 0;
45
0
    SOPC_ReturnStatus status = SOPC_Time_ToUnixTime(time, &seconds);
46
0
    if (SOPC_STATUS_OK != status)
47
0
    {
48
0
        return NULL;
49
0
    }
50
51
0
    uint32_t milliseconds = (uint32_t)((time / 10000) % 1000);
52
0
    struct tm tm;
53
54
0
    if (local)
55
0
    {
56
0
        status = SOPC_Time_Breakdown_Local(seconds, &tm);
57
0
    }
58
0
    else
59
0
    {
60
0
        status = SOPC_Time_Breakdown_UTC(seconds, &tm);
61
0
    }
62
63
0
    if (status != SOPC_STATUS_OK)
64
0
    {
65
0
        return NULL;
66
0
    }
67
68
0
    char* buf = SOPC_Calloc(buf_size, sizeof(char));
69
70
0
    if (buf == NULL)
71
0
    {
72
0
        return NULL;
73
0
    }
74
75
0
    size_t res = strftime(buf, buf_size - 1, compact ? "%Y%m%d_%H%M%S" : "%Y/%m/%d %H:%M:%S", &tm);
76
77
0
    if (res == 0)
78
0
    {
79
0
        SOPC_Free(buf);
80
0
        return NULL;
81
0
    }
82
83
0
    int res2 = snprintf(buf + res, 5, compact ? "_%03" PRIu32 : ".%03" PRIu32, milliseconds);
84
0
    SOPC_ASSERT(4 == res2);
85
86
0
    return buf;
87
0
}
88
89
static char* get_current_time_string(bool local, bool compact)
90
0
{
91
0
    return SOPC_Time_GetString(SOPC_Time_GetCurrentTimeUTC(), local, compact);
92
0
}
93
94
char* SOPC_Time_GetStringOfCurrentLocalTime(bool compact)
95
0
{
96
0
    return get_current_time_string(true, compact);
97
0
}
98
99
char* SOPC_Time_GetStringOfCurrentTimeUTC(bool compact)
100
0
{
101
0
    return get_current_time_string(false, compact);
102
0
}
103
104
SOPC_ReturnStatus SOPC_Time_FromUnixTime(SOPC_Unix_Time time, SOPC_DateTime* res)
105
0
{
106
0
    SOPC_ASSERT(time >= 0);
107
108
#if (SOPC_TIME_T_SIZE > 4)
109
    if (time > INT64_MAX)
110
    {
111
        return SOPC_STATUS_NOK;
112
    }
113
#endif
114
115
0
    int64_t dt = time;
116
117
0
    if (INT64_MAX - SOPC_SECONDS_BETWEEN_EPOCHS < dt)
118
0
    {
119
0
        return SOPC_STATUS_NOK;
120
0
    }
121
122
0
    dt += SOPC_SECONDS_BETWEEN_EPOCHS;
123
124
0
    if (INT64_MAX / SOPC_SECOND_TO_100_NANOSECONDS < dt)
125
0
    {
126
0
        return SOPC_STATUS_NOK;
127
0
    }
128
129
0
    dt *= SOPC_SECOND_TO_100_NANOSECONDS;
130
0
    *res = dt;
131
0
    return SOPC_STATUS_OK;
132
0
}
133
134
SOPC_ReturnStatus SOPC_Time_ToUnixTime(SOPC_DateTime dt, SOPC_Unix_Time* res)
135
0
{
136
0
    int64_t secs = dt / SOPC_SECOND_TO_100_NANOSECONDS;
137
138
0
    if (secs < SOPC_SECONDS_BETWEEN_EPOCHS)
139
0
    {
140
0
        return SOPC_STATUS_NOK;
141
0
    }
142
143
0
    secs -= SOPC_SECONDS_BETWEEN_EPOCHS;
144
145
0
    if (secs == (int64_t)(SOPC_Unix_Time) secs)
146
0
    {
147
0
        *res = (SOPC_Unix_Time) secs;
148
0
        return SOPC_STATUS_OK;
149
0
    }
150
0
    else
151
0
    {
152
0
        return SOPC_STATUS_NOK;
153
0
    }
154
0
}
155
156
static bool parseTwoDigitsUint8(const char* startPointer, size_t len, const char endChar, uint8_t* pOut)
157
0
{
158
0
    SOPC_ASSERT(NULL != startPointer);
159
0
    SOPC_ASSERT(NULL != pOut);
160
161
0
    if ((len > 2 && startPointer[2] != endChar) || len < 2)
162
0
    {
163
0
        return false;
164
0
    }
165
166
0
    SOPC_ReturnStatus status = SOPC_strtouint8_t(startPointer, pOut, 10, endChar);
167
0
    if (SOPC_STATUS_OK != status)
168
0
    {
169
0
        return false;
170
0
    }
171
172
0
    return true;
173
0
}
174
175
bool SOPC_tm_FromXsdDateTime(const char* datetime, size_t len, SOPC_tm* tm)
176
0
{
177
0
    if (NULL == tm)
178
0
    {
179
0
        return false;
180
0
    }
181
182
0
    SOPC_tm res;
183
0
    res.year = 0;
184
0
    res.month = 0;
185
0
    res.day = 0;
186
0
    res.hour = 0;
187
0
    res.minute = 0;
188
0
    res.second = 0;
189
0
    res.secondAndFrac = 0.0;
190
0
    res.UTC = true;
191
0
    res.UTC_neg_off = false;
192
0
    res.UTC_hour_off = 0;
193
0
    res.UTC_min_off = 0;
194
195
    // Check input: minimum length '<YYYY>-<MM>-<DD>T<hh>:<mm>:<ss>'
196
0
    if (NULL == datetime || len < 19)
197
0
    {
198
0
        return false;
199
0
    }
200
201
0
    const char* currentPointer = datetime;
202
0
    size_t remainingLength = len;
203
204
    /*
205
     * Parse year:
206
     * -?([1-9][0-9]{3,}|0[0-9]{3})-
207
     */
208
    // Check if year is prefixed by '-'
209
0
    const char* endPointer = NULL;
210
211
    // Manage the case of negative year by searching from next character
212
    // since at least 4 digits are expected in both cases
213
0
    endPointer = memchr(currentPointer + 1, '-', remainingLength - 1);
214
0
    if (NULL == endPointer || endPointer - currentPointer < (*currentPointer == '-' ? 5 : 4))
215
0
    {
216
        // End character not found or year < 4 digits year
217
0
        return false;
218
0
    }
219
220
0
    bool bres = SOPC_strtoint(currentPointer, (size_t)(endPointer - currentPointer), 16, &res.year);
221
0
    if (!bres)
222
0
    {
223
0
        return false;
224
0
    }
225
0
    endPointer++; // remove '-' end separator
226
0
    SOPC_ASSERT(endPointer > currentPointer);
227
0
    remainingLength -= (size_t)(endPointer - currentPointer);
228
0
    currentPointer = endPointer;
229
230
    /*
231
     * Parse Month:
232
     * (0[1-9]|1[0-2])-
233
     */
234
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, '-', &res.month);
235
0
    if (!bres || res.month < 1 || res.month > 12)
236
0
    {
237
0
        return false;
238
0
    }
239
0
    remainingLength -= 3;
240
0
    currentPointer += 3;
241
242
    /*
243
     * Parse Day:
244
     * (0[1-9]|[12][0-9]|3[01])T
245
     */
246
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, 'T', &res.day);
247
0
    if (!bres || res.day < 1 || res.day > 31)
248
0
    {
249
0
        return false;
250
0
    }
251
0
    remainingLength -= 3;
252
0
    currentPointer += 3;
253
254
    /*
255
     *  Check constraint: Day-of-month Values
256
     *  The day value must be no more than 30 if month is one of 4, 6, 9, or 11;
257
     *  no more than 28 if month is 2 and year is not divisible by 4,
258
     *  or is divisible by 100 but not by 400;
259
     *  and no more than 29 if month is 2 and year is divisible by 400, or by 4 but not by 100.
260
     */
261
0
    if (res.day > 30 && (4 == res.month || 6 == res.month || 9 == res.month || 11 == res.month))
262
0
    {
263
0
        return false;
264
0
    }
265
0
    if (res.day > 28 && (2 == res.month && (res.year % 4 != 0 || (0 == res.year % 100 && 0 != res.year % 400))))
266
0
    {
267
0
        return false;
268
0
    }
269
0
    if (res.day > 29 && 2 == res.month) // No more than 29 days in February
270
0
    {
271
0
        return false;
272
0
    }
273
274
    /*
275
     * Parse Hour:
276
     * ([01][0-9]|2[0-4]):
277
     */
278
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, ':', &res.hour);
279
    // Note: accept hour = 24 for case 24:00:00.0 to be check after parsing minutes and seconds
280
0
    if (!bres || res.hour > 24)
281
0
    {
282
0
        return false;
283
0
    }
284
0
    remainingLength -= 3;
285
0
    currentPointer += 3;
286
287
    /*
288
     * Parse Minutes:
289
     * ([0-5][0-9]):
290
     */
291
0
    bres = parseTwoDigitsUint8(currentPointer, remainingLength, ':', &res.minute);
292
293
0
    if (!bres || res.minute > 59)
294
0
    {
295
0
        return false;
296
0
    }
297
0
    remainingLength -= 3;
298
0
    currentPointer += 3;
299
300
    /*
301
     * Parse Seconds (whole number part):
302
     * ([0-5][0-9]):
303
     */
304
0
    if (remainingLength < 2)
305
0
    {
306
0
        return false;
307
0
    }
308
    // Use SOPC_strtouint to allow string ending without '\0' and avoid access out of string memory bounds
309
0
    bres = SOPC_strtouint(currentPointer, 2, 8, &res.second);
310
0
    if (!bres || res.second > 59)
311
0
    {
312
0
        return false;
313
0
    }
314
315
    // Initialize seconds without fraction first (in case of no fraction present)
316
0
    res.secondAndFrac = (double) res.second;
317
318
    // Check 24:00:00 special case
319
0
    if (24 == res.hour && (0 != res.minute || 0 != res.second))
320
0
    {
321
0
        return false;
322
0
    }
323
324
0
    if (2 == remainingLength)
325
0
    {
326
        // Whole datetime string consume without any error
327
0
        *tm = res;
328
329
0
        return true;
330
0
    }
331
332
    /*
333
     * Parse Seconds with fraction (if applicable)
334
     * [0-5][0-9](\.[0-9]+)?
335
     */
336
337
    // Check if there is a fraction of second
338
0
    if ('.' == currentPointer[2])
339
0
    {
340
        // Search for end character starting from character after the '.'
341
0
        endPointer = &currentPointer[3];
342
0
        size_t localRemLength = remainingLength - 3;
343
344
0
        while (localRemLength > 0 && *endPointer >= '0' && *endPointer <= '9')
345
0
        {
346
            // Check all digits are 0 if hour was 24
347
0
            if (24 == res.hour && '0' != *endPointer)
348
0
            {
349
0
                return false;
350
0
            }
351
0
            endPointer++;
352
0
            localRemLength--;
353
0
        }
354
355
        // Parse the seconds with fraction as a double value
356
0
        bres = SOPC_strtodouble(currentPointer, (size_t)(endPointer - currentPointer), 64, &res.secondAndFrac);
357
        // Note: we do not need to check for actual value since we already controlled each digit individually
358
        // If something went wrong it is either due to SOPC_strtodouble or due to double representation
359
0
        if (!bres)
360
0
        {
361
0
            return false;
362
0
        }
363
0
        remainingLength -= (size_t)(endPointer - currentPointer);
364
0
        currentPointer = endPointer;
365
0
    }
366
0
    else
367
0
    {
368
0
        remainingLength -= 2;
369
0
        currentPointer += 2;
370
0
    }
371
372
0
    if (0 != remainingLength && 'Z' != *currentPointer)
373
0
    {
374
        // Parse offset sign
375
0
        if ('-' == *currentPointer)
376
0
        {
377
0
            res.UTC_neg_off = true;
378
0
        }
379
0
        else if ('+' != *currentPointer)
380
0
        {
381
0
            return false;
382
0
        }
383
384
0
        remainingLength--;
385
0
        currentPointer++;
386
387
        /*
388
         * Parse Hour Offset:
389
         * (0[0-9]|1[0-4]):
390
         */
391
0
        bres = parseTwoDigitsUint8(currentPointer, remainingLength, ':', &res.UTC_hour_off);
392
        // Note: accept hour = 14 for case 14:00 to be check after parsing minutes and seconds
393
0
        if (!bres || res.UTC_hour_off > 14)
394
0
        {
395
0
            return false;
396
0
        }
397
0
        remainingLength -= 3;
398
0
        currentPointer += 3;
399
400
        /*
401
         * Parse Minute Offset
402
         * ([0-5][0-9]):
403
         */
404
0
        if (remainingLength < 2)
405
0
        {
406
0
            return false;
407
0
        }
408
        // Use SOPC_strtouint to allow string ending without '\0' and avoid access out of string memory bounds
409
0
        bres = SOPC_strtouint(currentPointer, 2, 8, &res.UTC_min_off);
410
        // Check for special case of 14:00 which is maximum offset value
411
0
        if (!bres || res.UTC_min_off > 59 || (14 == res.UTC_hour_off && 0 != res.UTC_min_off))
412
0
        {
413
0
            return false;
414
0
        }
415
416
0
        remainingLength -= 2;
417
0
        currentPointer += 2;
418
419
        // Set UTC flag regarding offset
420
0
        res.UTC = (0 == res.UTC_hour_off && 0 == res.UTC_min_off);
421
0
    }
422
0
    else if ('Z' == *currentPointer)
423
0
    {
424
0
        remainingLength--;
425
0
        currentPointer++;
426
0
    }
427
428
0
    if (0 != remainingLength)
429
0
    {
430
0
        return false;
431
0
    }
432
433
0
    *tm = res;
434
0
    return true;
435
0
}
436
437
static int64_t daysSince1601(int16_t year, uint8_t month, uint8_t day)
438
0
{
439
0
    SOPC_ASSERT(year >= 1601);
440
0
    SOPC_ASSERT(year <= 10000);
441
442
    // Years since 1601
443
0
    int16_t elapsedYearsSince1601 = (int16_t)(year - 1601);
444
445
    // Month-to-day offset for non-leap-years.
446
0
    const int64_t monthDaysElapsed[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
447
448
    // Number of February months since 01/01/1601
449
0
    int64_t nbFebs = elapsedYearsSince1601 + (1 - (month <= 2 ? 1 : 0));
450
451
    // Total number of leap days since 01/01/1601
452
0
    int64_t leapDays = (nbFebs / 4) - (nbFebs / 100) + (nbFebs / 400);
453
454
    // Total number of days =
455
    // 365 * elapsed years + elapsed leap days + elapsed days in current year before current month (without leap day)
456
    // + elapsed days in current month (- 1 since current day not elapsed yet)
457
0
    int64_t days = 365 * elapsedYearsSince1601 + leapDays + monthDaysElapsed[month - 1] + day - 1;
458
459
0
    return days;
460
0
}
461
462
static int64_t secondsSince1601(int16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second)
463
0
{
464
0
    SOPC_ASSERT(year >= 1601 || (year == 1600 && month == 12 && day == 31));
465
0
    SOPC_ASSERT(year <= 10000);
466
467
0
    if (year >= 1601) // number of seconds since 1601
468
0
    {
469
0
        const int64_t secsByDay = 86400;
470
0
        const int64_t secsCurrentDay = hour * 3600 + minute * 60 + second;
471
0
        const int64_t nbDaysSince1601 = daysSince1601(year, month, day);
472
473
0
        return secsByDay * nbDaysSince1601 + secsCurrentDay;
474
0
    }
475
0
    else // number of seconds until 01/01/1601 (from day 31/12/1600)
476
0
    {
477
0
        const int64_t secsUntil1601 = (24 - hour) * 3600 - minute * 60 - second;
478
        // negative number of seconds since reference is 1601
479
0
        return -1 * secsUntil1601;
480
0
    }
481
0
}
482
483
SOPC_ReturnStatus SOPC_Time_FromXsdDateTime(const char* dateTime, size_t len, int64_t* res)
484
0
{
485
    // 100ns as a fraction of second
486
0
    const float sec_fraction_100ns = (float) 0.0000001;
487
488
0
    SOPC_tm tm_res;
489
0
    memset(&tm_res, 0, sizeof(tm_res));
490
491
0
    bool parseRes = SOPC_tm_FromXsdDateTime(dateTime, len, &tm_res);
492
493
0
    if (!parseRes)
494
0
    {
495
0
        return SOPC_STATUS_INVALID_PARAMETERS;
496
0
    }
497
498
0
    if (tm_res.year < 1601 && (tm_res.year != 1600 || tm_res.month != 12 || tm_res.day != 31))
499
0
    {
500
        // A date/time value is encoded as 0 if is equal to or earlier than 1601-01-01 12:00AM UTC.
501
        // Due to timezone offset to be considered, we keep a 24:00:00 margin as a first approximation.
502
        // It excludes any date earlier than 31-12-1600.
503
0
        *res = 0;
504
0
        return SOPC_STATUS_OK;
505
0
    }
506
0
    else if (tm_res.year > 9999 && (tm_res.year != 10000 || tm_res.month != 1 || tm_res.day != 1))
507
0
    {
508
        // A date/time is encoded as the maximum value for an Int64 if
509
        // the value is equal to or greater than 9999-12-31 11:59:59PM UTC
510
        // Due to timezone offset to be considered, we keep a 24:00:00 margin as a first approximation.
511
        // It excludes any date greater than 01-01-10000.
512
0
        *res = INT64_MAX;
513
0
        return SOPC_STATUS_OK;
514
0
    }
515
516
    // Compute seconds since 1601
517
0
    int64_t secsSince1601 =
518
0
        secondsSince1601(tm_res.year, tm_res.month, tm_res.day, tm_res.hour, tm_res.minute, tm_res.second);
519
520
0
    if (!tm_res.UTC)
521
0
    {
522
0
        int64_t offset = tm_res.UTC_hour_off * 3600 + tm_res.UTC_min_off * 60;
523
0
        if (tm_res.UTC_neg_off)
524
0
        {
525
            // Negative offset, add the offset
526
0
            secsSince1601 += offset;
527
0
        }
528
0
        else
529
0
        {
530
            // Positive offset, substract the offset
531
0
            secsSince1601 -= offset;
532
0
        }
533
0
    }
534
535
    // Check date >= 1601-01-01 12:00AM UTC
536
0
    if (secsSince1601 < 0)
537
0
    {
538
        // A date/time value is encoded as 0 if is equal to or earlier than 1601-01-01 12:00AM UTC.
539
        // Due to timezone offset to be considered, we keep a 24:00:00 margin as a first approximation.
540
        // It excludes any date earlier than 31-12-1600.
541
0
        *res = 0;
542
0
        return SOPC_STATUS_OK;
543
0
    }
544
0
    else if (secsSince1601 >= 265046774399) // Check date < 9999-12-31 11:59:59PM UTC
545
0
    {
546
        // Note: 265046774399 == secondsSince1601(9999, 12, 31, 23, 59, 59)
547
548
        // A date/time is encoded as the maximum value for an Int64 if
549
        // the value is equal to or greater than 9999-12-31 11:59:59PM UTC
550
0
        *res = INT64_MAX;
551
0
        return SOPC_STATUS_OK;
552
0
    }
553
554
    // Compute seconds fraction if significant
555
0
    double sec_fraction = tm_res.secondAndFrac - (double) tm_res.second;
556
0
    int64_t hundredOfNanoseconds = (int64_t)(sec_fraction / (double) sec_fraction_100ns);
557
558
    // Note: no overflow possible for 1601 <= year <= 10000
559
0
    *res = secsSince1601 * SOPC_SECOND_TO_100_NANOSECONDS + hundredOfNanoseconds;
560
0
    return SOPC_STATUS_OK;
561
0
}