logo

utils-std

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

iso_parse.c (3798B)


  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. *nsec = 0;
  23. // For Alpine's abuild compatibility
  24. if(arg[0] == '@')
  25. {
  26. arg++;
  27. char *endptr = NULL;
  28. time_t now = strtol(arg, &endptr, 10);
  29. if(errno != 0)
  30. {
  31. *errstr = strerror(errno);
  32. errno = 0;
  33. return NULL;
  34. }
  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. #ifndef TZNAME_MAX
  97. #define TZNAME_MAX _POSIX_TZNAME_MAX
  98. #endif
  99. #if TZNAME_MAX < 5
  100. #error TZNAME_MAX is too small
  101. #endif
  102. static char offname[TZNAME_MAX + 1] = "";
  103. int neg;
  104. if(s[0] == '+')
  105. neg = 0;
  106. else if(s[0] == '-')
  107. neg = 1;
  108. else
  109. {
  110. *errstr = "Invalid timezone offset, must start with + or -";
  111. return NULL;
  112. }
  113. size_t offname_i = 0;
  114. offname[offname_i++] = *s++;
  115. if(isdigit(s[0]) && isdigit(s[1]))
  116. {
  117. time->tm_gmtoff = (s[0] - '0') * 36000 + (s[1] - '0') * 3600;
  118. offname[offname_i++] = *s++;
  119. offname[offname_i++] = *s++;
  120. }
  121. else
  122. {
  123. *errstr = "Invalid timezone offset, no digits after <+|->";
  124. return NULL;
  125. }
  126. if(s[0] == ':') s++;
  127. if(isdigit(s[0]) && isdigit(s[1]))
  128. {
  129. time->tm_gmtoff += (s[0] - '0') * 600 + (s[1] - '0') * 60;
  130. offname[offname_i++] = *s++;
  131. offname[offname_i++] = *s++;
  132. }
  133. else
  134. {
  135. *errstr = "Invalid timezone offset, no digits after <+|->HH[:]";
  136. return NULL;
  137. }
  138. if(neg) time->tm_gmtoff = -time->tm_gmtoff;
  139. offname[offname_i++] = '\0';
  140. time->tm_zone = offname;
  141. }
  142. }
  143. return s;
  144. }
  145. // Because mktime() messes with tm_gmtoff yet doesn't applies it, even in POSIX.1-2024
  146. // Returns (time_t)-1 on failure
  147. time_t
  148. mktime_tz(struct tm *tm)
  149. {
  150. long gmtoff = tm->tm_gmtoff;
  151. const char *zone = tm->tm_zone;
  152. time_t res = mktime(tm);
  153. tm->tm_gmtoff = gmtoff;
  154. tm->tm_zone = zone;
  155. if(res == (time_t)-1) return res;
  156. // 12:00+02:00 corresponds to 10:00Z so needs to be reversed
  157. res += -gmtoff;
  158. return res;
  159. }