| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "../libnetdata.h" |
| 4 | |
| 5 | #include "rfc3339.h" |
| 6 | |
| 7 | // Helper functions for safe printing of date/time components |
| 8 | // These functions don't add a null terminator and return only the exact count of bytes written |
| 9 | |
| 10 | static inline size_t print_4digit_year(char *buffer, size_t size, int year) { |
| 11 | if (!buffer || size < 4) return 0; // Need at least 4 digits |
| 12 | |
| 13 | // Ensure year is in valid range (0-9999) |
| 14 | year = year < 0 ? 0 : (year > 9999 ? 9999 : year); |
| 15 | |
| 16 | buffer[0] = '0' + (year / 1000) % 10; |
| 17 | buffer[1] = '0' + (year / 100) % 10; |
| 18 | buffer[2] = '0' + (year / 10) % 10; |
| 19 | buffer[3] = '0' + year % 10; |
| 20 | |
| 21 | return 4; |
| 22 | } |
| 23 | |
| 24 | static inline size_t print_2digit(char *buffer, size_t size, int value) { |
| 25 | if (!buffer || size < 2) return 0; // Need at least 2 digits |
| 26 | |
| 27 | // Ensure value is in valid range (0-99) |
| 28 | value = value < 0 ? 0 : (value > 99 ? 99 : value); |
| 29 | |
| 30 | buffer[0] = '0' + (value / 10) % 10; |
| 31 | buffer[1] = '0' + value % 10; |
| 32 | |
| 33 | return 2; |
| 34 | } |
| 35 | |
| 36 | static inline size_t print_fraction(char *buffer, size_t size, usec_t fraction, size_t digits) { |
| 37 | if (!buffer) return 0; |
| 38 | |
| 39 | // Validate and cap the number of digits |
| 40 | digits = digits < 1 ? 1 : (digits > 9 ? 9 : digits); |
| 41 | if (size < digits) return 0; |
| 42 | |
| 43 | // Scale microseconds to the requested precision |
| 44 | if (digits < 6) { |
| 45 | usec_t divisor = 1; |
| 46 | for (size_t i = digits; i < 6; i++) |
| 47 | divisor *= 10; |
| 48 | |
| 49 | fraction /= divisor; |
| 50 | } |
| 51 | else if (digits > 6) { |
| 52 | for (size_t i = 6; i < digits; i++) |
| 53 | fraction *= 10; |
| 54 | } |
| 55 | |
| 56 | // Ensure fraction won't exceed the requested number of digits |
| 57 | usec_t max_value = 1; |
| 58 | for (size_t i = 0; i < digits; i++) |
| 59 | max_value *= 10; |
| 60 | max_value--; |
| 61 | |
| 62 | fraction = fraction > max_value ? max_value : fraction; |
| 63 | |
| 64 | // Print the fraction with leading zeros |
| 65 | usec_t remaining = fraction; |
| 66 | |
| 67 | // Setup working backwards from least significant digit |
| 68 | for (int i = digits - 1; i >= 0; i--) { |
| 69 | buffer[i] = '0' + (remaining % 10); |
| 70 | remaining /= 10; |
| 71 | } |
| 72 | |
| 73 | return digits; |
| 74 | } |
| 75 | |
| 76 | size_t rfc3339_datetime_ut(char *buffer, size_t len, usec_t now_ut, size_t fractional_digits, bool utc) { |
| 77 | if (!buffer || len < 20) // Minimum size for YYYY-MM-DDThh:mm:ssZ |
| 78 | return 0; |
| 79 | |
| 80 | time_t t = (time_t)(now_ut / USEC_PER_SEC); |
| 81 | struct tm *tmp, tmbuf; |
| 82 | |
| 83 | if (utc) |
| 84 | tmp = gmtime_r(&t, &tmbuf); |
| 85 | else |
| 86 | tmp = localtime_r(&t, &tmbuf); |
| 87 | |
| 88 | if (!tmp) { |
| 89 | buffer[0] = '\0'; |
| 90 | return 0; |
| 91 | } |
| 92 | |
| 93 | size_t pos = 0; |
| 94 | |
| 95 | // Year (4 digits) |
| 96 | if (len - pos < 4) goto finish; |
| 97 | pos += print_4digit_year(&buffer[pos], len - pos, tmp->tm_year + 1900); |
| 98 | |
| 99 | // Month separator |
| 100 | if (len - pos < 1) goto finish; |
| 101 | buffer[pos++] = '-'; |
| 102 | |
| 103 | // Month (2 digits) |
| 104 | if (len - pos < 2) goto finish; |
| 105 | pos += print_2digit(&buffer[pos], len - pos, tmp->tm_mon + 1); |
| 106 | |
| 107 | // Day separator |
| 108 | if (len - pos < 1) goto finish; |
| 109 | buffer[pos++] = '-'; |
| 110 | |
| 111 | // Day (2 digits) |
| 112 | if (len - pos < 2) goto finish; |
| 113 | pos += print_2digit(&buffer[pos], len - pos, tmp->tm_mday); |
| 114 | |
| 115 | // T separator |
| 116 | if (len - pos < 1) goto finish; |
| 117 | buffer[pos++] = 'T'; |
| 118 | |
| 119 | // Hour (2 digits) |
| 120 | if (len - pos < 2) goto finish; |
| 121 | pos += print_2digit(&buffer[pos], len - pos, tmp->tm_hour); |
| 122 | |
| 123 | // Minute separator |
| 124 | if (len - pos < 1) goto finish; |
| 125 | buffer[pos++] = ':'; |
| 126 | |
| 127 | // Minute (2 digits) |
| 128 | if (len - pos < 2) goto finish; |
| 129 | pos += print_2digit(&buffer[pos], len - pos, tmp->tm_min); |
| 130 | |
| 131 | // Second separator |
| 132 | if (len - pos < 1) goto finish; |
| 133 | buffer[pos++] = ':'; |
| 134 | |
| 135 | // Second (2 digits) |
| 136 | if (len - pos < 2) goto finish; |
| 137 | pos += print_2digit(&buffer[pos], len - pos, tmp->tm_sec); |
| 138 | |
| 139 | // Add fractional part if requested |
| 140 | if (fractional_digits > 9) fractional_digits = 9; |
| 141 | if (fractional_digits) { |
| 142 | usec_t fractional_part = now_ut % USEC_PER_SEC; |
| 143 | |
| 144 | if (fractional_part > 0) { |
| 145 | // Need space for decimal point and digits |
| 146 | if (len - pos < fractional_digits + 1) goto finish; |
| 147 | |
| 148 | buffer[pos++] = '.'; |
| 149 | pos += print_fraction(&buffer[pos], len - pos, fractional_part, fractional_digits); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | // Add timezone information |
| 154 | if (utc) { |
| 155 | if (len - pos < 1) goto finish; |
| 156 | buffer[pos++] = 'Z'; |
| 157 | } |
| 158 | else { |
| 159 | long offset = tmbuf.tm_gmtoff; |
| 160 | int hours = (int)(offset / 3600); |
| 161 | int minutes = abs((int)((offset % 3600) / 60)); |
| 162 | |
| 163 | // Check if timezone is UTC |
| 164 | if (hours == 0 && minutes == 0) { |
| 165 | if (len - pos < 1) goto finish; |
| 166 | buffer[pos++] = 'Z'; |
| 167 | } |
| 168 | else { |
| 169 | // Need space for sign, hours, colon, minutes (6 chars total) |
| 170 | if (len - pos < 6) goto finish; |
| 171 | |
| 172 | // Add timezone offset |
| 173 | buffer[pos++] = (hours >= 0) ? '+' : '-'; |
| 174 | hours = abs(hours); |
| 175 | |
| 176 | // Hours with leading zero |
| 177 | pos += print_2digit(&buffer[pos], len - pos, hours); |
| 178 | |
| 179 | // Colon |
| 180 | buffer[pos++] = ':'; |
| 181 | |
| 182 | // Minutes with leading zero |
| 183 | pos += print_2digit(&buffer[pos], len - pos, minutes); |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | finish: |
| 188 | // Ensure null termination |
| 189 | if (pos < len) |
| 190 | buffer[pos] = '\0'; |
| 191 | else |
| 192 | buffer[len - 1] = '\0'; |
| 193 | |
| 194 | return pos; |
| 195 | } |
| 196 | |
| 197 | usec_t rfc3339_parse_ut(const char *rfc3339, char **endptr) { |
| 198 | struct tm tm = { 0 }; |
| 199 | int tz_hours = 0, tz_mins = 0; |
| 200 | char *s; |
| 201 | usec_t timestamp, usec = 0; |
| 202 | |
| 203 | // Parse date and time (up to seconds) |
| 204 | s = strptime(rfc3339, "%Y-%m-%dT%H:%M:%S", &tm); |
| 205 | if (!s) |
| 206 | return 0; // Parsing error |
| 207 | |
| 208 | // Parse fractional seconds if present |
| 209 | if (*s == '.') { |
| 210 | char *next; |
| 211 | usec = strtoul(s + 1, &next, 10); |
| 212 | int digits_parsed = (int)(next - (s + 1)); |
| 213 | |
| 214 | if (digits_parsed < 1 || digits_parsed > 9) |
| 215 | return 0; // Parsing error |
| 216 | |
| 217 | static const usec_t fix_usec[] = { |
| 218 | 1000000, // 0 digits (not used) |
| 219 | 100000, // 1 digit |
| 220 | 10000, // 2 digits |
| 221 | 1000, // 3 digits |
| 222 | 100, // 4 digits |
| 223 | 10, // 5 digits |
| 224 | 1, // 6 digits |
| 225 | 10, // 7 digits |
| 226 | 100, // 8 digits |
| 227 | 1000 // 9 digits |
| 228 | }; |
| 229 | |
| 230 | if (digits_parsed <= 6) |
| 231 | usec = usec * fix_usec[digits_parsed]; |
| 232 | else |
| 233 | usec = usec / fix_usec[digits_parsed]; |
| 234 | |
| 235 | s = next; |
| 236 | } |
| 237 | |
| 238 | // Parse timezone specification |
| 239 | int tz_offset = 0; |
| 240 | if (*s == '+' || *s == '-') { |
| 241 | // Ensure format is correct: e.g. +02:00 or -05:30 |
| 242 | |
| 243 | if (!isdigit((uint8_t)s[1]) || !isdigit((uint8_t)s[2]) || s[3] != ':' || |
| 244 | !isdigit((uint8_t)s[4]) || !isdigit((uint8_t)s[5])) |
| 245 | return 0; // Parsing error |
| 246 | |
| 247 | char tz_sign = *s; |
| 248 | tz_hours = (s[1] - '0') * 10 + (s[2] - '0'); |
| 249 | tz_mins = (s[4] - '0') * 10 + (s[5] - '0'); |
| 250 | tz_offset = tz_hours * 3600 + tz_mins * 60; |
| 251 | tz_offset *= (tz_sign == '+' ? 1 : -1); |
| 252 | |
| 253 | s += 6; // Move past the timezone part |
| 254 | } |
| 255 | else if (*s == 'Z') |
| 256 | s++; |
| 257 | else |
| 258 | return 0; // Invalid RFC 3339 timezone specification |
| 259 | |
| 260 | // Convert struct tm to time_t in UTC |
| 261 | time_t epoch_s; |
| 262 | |
| 263 | #if defined(HAVE_TIMEGM) |
| 264 | // If available, use timegm() which interprets tm as UTC. |
| 265 | epoch_s = timegm(&tm); |
| 266 | #else |
| 267 | // Use mktime(), which assumes tm is local time, then adjust. |
| 268 | epoch_s = mktime(&tm); |
| 269 | if (epoch_s == -1) |
| 270 | return 0; // Error in time conversion |
| 271 | |
| 272 | # if defined(HAVE_TM_GMTOFF) |
| 273 | // tm.tm_gmtoff is the offset (in seconds) of local time from UTC. |
| 274 | epoch_s -= tm.tm_gmtoff; |
| 275 | # else |
| 276 | // Fallback: compute the difference between localtime and gmtime. |
| 277 | { |
| 278 | struct tm local_tm, utc_tm; |
| 279 | #if defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(__APPLE__) |
| 280 | localtime_r(&epoch_s, &local_tm); |
| 281 | gmtime_r(&epoch_s, &utc_tm); |
| 282 | #else |
| 283 | // If thread-safe functions are not available, use localtime() and gmtime() |
| 284 | struct tm *lt = localtime(&epoch_s); |
| 285 | struct tm *gt = gmtime(&epoch_s); |
| 286 | if (!lt || !gt) |
| 287 | return 0; |
| 288 | local_tm = *lt; |
| 289 | utc_tm = *gt; |
| 290 | #endif |
| 291 | int local_offset = (local_tm.tm_hour - utc_tm.tm_hour) * 3600 + |
| 292 | (local_tm.tm_min - utc_tm.tm_min) * 60; |
| 293 | int day_diff = local_tm.tm_yday - utc_tm.tm_yday; |
| 294 | local_offset += day_diff * 86400; |
| 295 | epoch_s -= local_offset; |
| 296 | } |
| 297 | # endif |
| 298 | #endif |
| 299 | |
| 300 | // Combine seconds with fractional microseconds, then adjust for the RFC 3339 timezone. |
| 301 | timestamp = (usec_t)epoch_s * USEC_PER_SEC + usec; |
| 302 | timestamp -= tz_offset * USEC_PER_SEC; |
| 303 | |
| 304 | if (endptr) |
| 305 | *endptr = s; |
| 306 | |
| 307 | return timestamp; |
| 308 | } |