logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/

iso_parse.c (3613B)


  1. // utils-std: Collection of commonly available Unix tools
  2. // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
  3. // SPDX-License-Identifier: MPL-2.0
  4. #define _DEFAULT_SOURCE // tm_gmtoff/tm_zone
  5. #define _XOPEN_SOURCE 700 // strptime (NetBSD)
  6. #define _POSIX_C_SOURCE 200809L // st_atim/st_mtim
  7. #include "./iso_parse.h"
  8. #include <assert.h>
  9. #include <ctype.h> /* isdigit */
  10. #include <errno.h> /* errno */
  11. #include <inttypes.h> /* PRId16 */
  12. #include <limits.h> /* TZNAME_MAX */
  13. #include <stdio.h> /* perror, sscanf */
  14. #include <stdlib.h> /* strtol */
  15. #include <string.h> /* memset */
  16. #include <time.h> /* strptime, tm */
  17. // Sets errstr on failure
  18. // YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?(Z|[+\-]hh:?mm)?
  19. char *
  20. iso_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
  21. {
  22. // For Alpine's abuild compatibility
  23. if(arg[0] == '@')
  24. {
  25. arg++;
  26. char *endptr = NULL;
  27. time_t now = strtol(arg, &endptr, 10);
  28. if(errno != 0)
  29. {
  30. *errstr = strerror(errno);
  31. errno = 0;
  32. return NULL;
  33. }
  34. nsec = 0;
  35. gmtime_r(&now, time);
  36. return endptr;
  37. }
  38. // No %F in POSIX
  39. char *s = strptime(arg, "%Y-%m-%d", time);
  40. if(s == NULL)
  41. {
  42. *errstr = "strptime(…, \"%Y-%m-%d\", …) returned NULL";
  43. errno = 0;
  44. return NULL;
  45. }
  46. if(s[0] != 'T' && s[0] != ' ')
  47. {
  48. *errstr = "Couldn't find time-separator (T or space) after date (Y-m-d)";
  49. errno = 0;
  50. return NULL;
  51. }
  52. s++;
  53. s = strptime(s, "%H:%M:%S", time);
  54. if(s == NULL)
  55. {
  56. *errstr = "strptime(…, \"%H:%M:%S\", …) returned NULL";
  57. errno = 0;
  58. return NULL;
  59. }
  60. if(s[0] == ',' || s[0] == '.')
  61. {
  62. double fraction = 0.0;
  63. int parsed = 0;
  64. if(s[0] == ',') s[0] = '.';
  65. if(sscanf(s, "%10lf%n", &fraction, &parsed) < 1)
  66. {
  67. if(errno == 0)
  68. {
  69. *errstr = "Failed to parse fractional seconds";
  70. }
  71. else
  72. {
  73. *errstr = strerror(errno);
  74. errno = 0;
  75. }
  76. return NULL;
  77. }
  78. *nsec = (long)(fraction * 1000000000);
  79. s += parsed;
  80. // too many digits
  81. if(isdigit(s[0]))
  82. {
  83. *errstr = "Too many digits (> 10) for fractional seconds";
  84. return NULL;
  85. }
  86. }
  87. if(s != NULL && s[0] != '\0')
  88. {
  89. if(s[0] == 'Z' && s[1] == '\0')
  90. {
  91. time->tm_gmtoff = 0;
  92. time->tm_zone = "UTC";
  93. }
  94. else
  95. {
  96. int neg;
  97. if(*s == '+')
  98. neg = 0;
  99. else if(*s == '-')
  100. neg = 1;
  101. else
  102. {
  103. *errstr = "Invalid timezone offset, must start with + or -";
  104. return NULL;
  105. }
  106. char *o = s + 1;
  107. if(isdigit(o[0]) && isdigit(o[1]))
  108. {
  109. time->tm_gmtoff = (o[0] - '0') * 36000 + (o[1] - '0') * 3600;
  110. o += 2;
  111. }
  112. else
  113. {
  114. *errstr = "Invalid timezone offset, no digits after [+|-]";
  115. return NULL;
  116. }
  117. if(o[0] == ':') o++;
  118. if(isdigit(o[0]) && isdigit(o[1]))
  119. {
  120. time->tm_gmtoff += (o[0] - '0') * 600 + (o[1] - '0') * 60;
  121. o += 2;
  122. }
  123. else
  124. {
  125. *errstr = "Invalid timezone offset, no digits after [+|-]";
  126. return NULL;
  127. }
  128. if(neg) time->tm_gmtoff = -time->tm_gmtoff;
  129. #ifndef TZNAME_MAX
  130. #define TZNAME_MAX _POSIX_TZNAME_MAX
  131. #endif
  132. static char offname[TZNAME_MAX + 1] = "";
  133. assert(o - s < TZNAME_MAX);
  134. memcpy(offname, s, o - s);
  135. time->tm_zone = offname;
  136. }
  137. }
  138. return s;
  139. }
  140. // Because mktime() messes with tm_gmtoff yet doesn't applies it, even in POSIX.1-2024
  141. // Returns (time_t)-1 on failure
  142. time_t
  143. mktime_tz(struct tm *tm)
  144. {
  145. long gmtoff = tm->tm_gmtoff;
  146. const char *zone = tm->tm_zone;
  147. time_t res = mktime(tm);
  148. tm->tm_gmtoff = gmtoff;
  149. tm->tm_zone = zone;
  150. if(res == (time_t)-1) return res;
  151. // 12:00+02:00 corresponds to 10:00Z so needs to be reversed
  152. res += -gmtoff;
  153. return res;
  154. }