Add strptime(3).

This commit is contained in:
Jonas 'Sortie' Termansen 2023-03-19 15:02:18 +01:00
parent 4ac7072f2a
commit 693f57a0e3
3 changed files with 315 additions and 3 deletions

View File

@ -258,6 +258,7 @@ time/mktime.o \
timespec/timespec.o \
time/strftime_l.o \
time/strftime.o \
time/strptime.o \
time/timegm.o \
ubsan/ubsan.o \
wchar/btowc.o \

314
libc/time/strptime.c Normal file
View File

@ -0,0 +1,314 @@
/*
* Copyright (c) 2023 Jonas 'Sortie' Termansen.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* time/strptime.c
* Parse date and time.
*/
#include <ctype.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#ifdef TEST
#include <stdio.h>
#define strptime mystrptime
#endif
static const char* wdays[] = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", NULL};
static const char* months[] = {"January", "February", "March", "April", "May",
"June", "July", "August", "September", "October",
"November", "December", NULL};
static const char* strptime_str(const char* str,
int* output,
const char* const* list)
{
for ( int i = 0; list[i]; i++ )
{
size_t len = strlen(list[i]);
if ( !strncasecmp(str, list[i], len) )
return *output = i, str + len;
if ( !strncasecmp(str, list[i], 3) )
return *output = i, str + 3;
}
return NULL;
}
static const char* strptime_num(const char* str,
int* output,
int minimum,
int maximum,
size_t min_digits,
size_t max_digits,
int offset)
{
int value = 0;
size_t i = 0;
while ( i < max_digits )
{
if ( str[i] < '0'|| '9' < str[i] )
{
if ( min_digits <= i )
break;
return NULL;
}
if ( __builtin_mul_overflow(value, 10, &value) ||
__builtin_add_overflow(value, str[i++] - '0', &value) )
return NULL;
}
str += i;
if ( value < minimum || maximum < value )
return NULL;
if ( __builtin_add_overflow(value, offset, &value) )
return NULL;
return *output = value, str;
}
char* strptime(const char* restrict str,
const char* restrict format,
struct tm* restrict tm)
{
bool pm = false;
bool need_mktime = false;
int year_high = -1, year_low = -1;
for ( size_t i = 0; format[i]; )
{
if ( isspace((unsigned char) format[i]) )
{
do i++;
while ( isspace((unsigned char) format[i]) );
if ( !isspace((unsigned char) *str) )
return NULL;
do str++;
while ( isspace((unsigned char) *str) );
continue;
}
else if ( format[i] != '%' )
{
if ( format[i++] != *str++ )
return NULL;
continue;
}
i++;
if ( format[i] == '0' || format[i] == '+' )
i++;
bool has_width = '0' <= format[i] && format[i] <= '9';
size_t width = 0;
while ( '0' <= format[i] && format[i] <= '9' )
width = width * 10 + (format[i++] - '0');
// TODO: Maximum width.
bool modifier_E = false, modifier_O = false;
if ( format[i] == 'E' )
modifier_E = true, i++;
else if ( format[i] == 'O' )
modifier_O = true, i++;
(void) modifier_E, (void) modifier_O;
switch ( format[i] )
{
case 'a':
case 'A': str = strptime_str(str, &tm->tm_wday, wdays); break;
case 'b':
case 'B':
case 'h':
str = strptime_str(str, &tm->tm_mon, months);
need_mktime = true;
break;
case 'c': str = strptime(str, "%a %b %e %H:%M:%S %Y", tm); break;
case 'C': str = strptime_num(str, &year_high, 0, 99, 1, 2, 0); break;
case 'd':
case 'e':
str = strptime_num(str, &tm->tm_mday, 1, 31, 1, 2, 0);
need_mktime = true;
break;
case 'D': str = strptime(str, "%m/%d/%y", tm); break;
case 'H': str = strptime_num(str, &tm->tm_hour, 0, 23, 1, 2, 0); break;
case 'I':
str = strptime_num(str, &tm->tm_hour, 1, 12, 1, 2, 0);
if ( tm->tm_hour == 12 )
tm->tm_hour = 0;
break;
case 'j': str = strptime_num(str, &tm->tm_yday, 1, 366, 3, 3, -1); break;
case 'm':
str = strptime_num(str, &tm->tm_mon, 1, 12, 1, 2, -1);
need_mktime = true;
break;
case 'M': str = strptime_num(str, &tm->tm_min, 0, 59, 1, 2, 0); break;
case 'n':
case 't':
if ( !isspace((unsigned char) *str) )
return NULL;
do str++;
while ( isspace((unsigned char) *str) );
break;
case 'p':
if ( !strncasecmp(str, "am", 2) )
str += 2, pm = false;
else if ( !strncasecmp(str, "pm", 2) )
str += 2, pm = true;
else
return NULL;
break;
case 'r': str = strptime(str, "%I:%M:%S %p", tm); break;
case 'R': str = strptime(str, "%H:%M", tm); break;
case 'S': str = strptime_num(str, &tm->tm_sec, 0, 60, 1, 2, 0); break;
case 'T': str = strptime(str, "%H:%M:%S", tm); break;
// TODO: %U week number
case 'w': str = strptime_num(str, &tm->tm_wday, 0, 6, 1, 1, 0); break;
// TODO: %W week number
case 'x': str = strptime(str, "%m/%d/%Y", tm); break;
case 'X': str = strptime(str, "%H:%M:%S", tm); break;
case 'y': str = strptime_num(str, &year_low, 0, 99, 1, 2, 0); break;
case 'Y':
// POSIX divergence: Avoid year 10k problem by allowing more than
// four characters by default.
if ( !has_width )
width = SIZE_MAX;
// TODO: Allow leading + and - per POSIX.
str = strptime_num(str, &tm->tm_year, INT_MIN, INT_MAX, 1, width,
-1900);
need_mktime = true;
break;
case 'z':
// TODO: More exact.
if ( *str != '-' || *str != '+' )
return NULL;
int hours, minutes;
if ( !(str = strptime_num(str, &hours, -12, 12, 2, 2, 0)) ||
!(str = strptime_num(str, &minutes, 0, 59, 2, 2, 0)) )
return NULL;
tm->tm_isdst = 0;
// TODO: What is done with this timezone information?
break;
case 'Z':
// TODO: Other timezones.
if ( strncmp(str, "UTC", 3) != 0 )
return NULL;
str += 3;
tm->tm_isdst = 0;
// TODO: What is done with this timezone information?
break;
case '%':
if ( *str++ != '%' )
return NULL;
break;
default: NULL;
}
if ( !str )
return NULL;
i++;
}
if ( str )
{
if ( pm )
tm->tm_hour += 12;
if ( year_high != -1 || year_low != -1 )
{
if ( year_high == -1 )
year_high = year_low < 70 ? 20 : 19;
else if ( year_low == -1 )
year_low = 0;
tm->tm_year = year_high * 100 + year_low - 1900;
need_mktime = true;
}
if ( need_mktime )
{
struct tm copy = *tm;
mktime(&copy);
tm->tm_wday = copy.tm_wday;
tm->tm_yday = copy.tm_yday;
}
}
return (char*) str;
}
#ifdef TEST
#undef strptime
#include <err.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
if ( argc < 3 )
err(1, "usage");
const char* str = argv[1];
const char* format = argv[2];
struct tm my_tm = {0};
char* my_end = mystrptime(str, format, &my_tm);
struct tm c_tm = {0};
char* c_end = strptime(str, format, &c_tm);
if ( !my_end && c_end )
errx(1, "rejected but c allowed it");
else if ( !my_end )
errx(1, "rejected correctly");
else if ( !c_end )
printf("allowed but c rejected\n");
else if ( my_end != c_end )
errx(1, "mismatch my end \"%s\" vs c end \"%s\"", my_end, c_end);
if ( my_tm.tm_sec == c_tm.tm_sec )
printf("tm_sec=%i\n", my_tm.tm_sec);
else
printf("tm_sec=%i but C is %i\n", my_tm.tm_sec, c_tm.tm_sec);
if ( my_tm.tm_min == c_tm.tm_min )
printf("tm_min=%i\n", my_tm.tm_min);
else
printf("tm_min=%i but C is %i\n", my_tm.tm_min, c_tm.tm_min);
if ( my_tm.tm_hour == c_tm.tm_hour )
printf("tm_hour=%i\n", my_tm.tm_hour);
else
printf("tm_hour=%i but C is %i\n", my_tm.tm_hour, c_tm.tm_hour);
if ( my_tm.tm_mday == c_tm.tm_mday )
printf("tm_mday=%i\n", my_tm.tm_mday);
else
printf("tm_mday=%i but C is %i\n", my_tm.tm_mday, c_tm.tm_mday);
if ( my_tm.tm_mon == c_tm.tm_mon )
printf("tm_mon=%i\n", my_tm.tm_mon);
else
printf("tm_mon=%i but C is %i\n", my_tm.tm_mon, c_tm.tm_mon);
if ( my_tm.tm_year == c_tm.tm_year )
printf("tm_year=%i\n", my_tm.tm_year);
else
printf("tm_year=%i but C is %i\n", my_tm.tm_year, c_tm.tm_year);
if ( my_tm.tm_wday == c_tm.tm_wday )
printf("tm_wday=%i\n", my_tm.tm_wday);
else
printf("tm_wday=%i but C is %i\n", my_tm.tm_wday, c_tm.tm_wday);
if ( my_tm.tm_yday == c_tm.tm_yday )
printf("tm_yday=%i\n", my_tm.tm_yday);
else
printf("tm_yday=%i but C is %i\n", my_tm.tm_yday, c_tm.tm_yday);
if ( my_tm.tm_isdst == c_tm.tm_isdst )
printf("tm_isdst=%i\n", my_tm.tm_isdst);
else
printf("tm_isdst=%i but C is %i\n", my_tm.tm_isdst, c_tm.tm_isdst);
return 0;
}
#endif

View File

@ -243,9 +243,6 @@ should be use instead as the destination buffer size should always be known,
otherwise the invocation is suspicious.
The superior alternative is to combine allocation and initialization using
.Xr asprintf 3 .
.Ss strptime
.Xr strptime 3
is not currently implemented.
.Ss <sys/param.h>
.In sys/param.h
is not implemented as there is little agreement on what it's supposed to contain