1 /* $NetBSD: parse-duration.c,v 1.11 2024/08/18 20:47:25 christos Exp $ */ 2 3 /* Parse a time duration and return a seconds count 4 Copyright (C) 2008-2018 Free Software Foundation, Inc. 5 Written by Bruce Korb <bkorb@gnu.org>, 2008. 6 7 This program is free software: you can redistribute it and/or modify 8 it under the terms of the GNU Lesser General Public License as published by 9 the Free Software Foundation; either version 2.1 of the License, or 10 (at your option) any later version. 11 12 This program is distributed in the hope that it will be useful, 13 but WITHOUT ANY WARRANTY; without even the implied warranty of 14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 GNU Lesser General Public License for more details. 16 17 You should have received a copy of the GNU Lesser General Public License 18 along with this program. If not, see <https://www.gnu.org/licenses/>. */ 19 20 #include <config.h> 21 22 /* Specification. */ 23 #include "parse-duration.h" 24 25 #include <ctype.h> 26 #include <errno.h> 27 #include <limits.h> 28 #include <stdio.h> 29 #include <stdlib.h> 30 #include <string.h> 31 32 #include "intprops.h" 33 34 #ifndef NUL 35 #define NUL '\0' 36 #endif 37 38 #define cch_t char const 39 40 typedef enum { 41 NOTHING_IS_DONE, 42 YEAR_IS_DONE, 43 MONTH_IS_DONE, 44 WEEK_IS_DONE, 45 DAY_IS_DONE, 46 HOUR_IS_DONE, 47 MINUTE_IS_DONE, 48 SECOND_IS_DONE 49 } whats_done_t; 50 51 #define SEC_PER_MIN 60 52 #define SEC_PER_HR (SEC_PER_MIN * 60) 53 #define SEC_PER_DAY (SEC_PER_HR * 24) 54 #define SEC_PER_WEEK (SEC_PER_DAY * 7) 55 #define SEC_PER_MONTH (SEC_PER_DAY * 30) 56 #define SEC_PER_YEAR (SEC_PER_DAY * 365) 57 58 #undef MAX_DURATION 59 #define MAX_DURATION TYPE_MAXIMUM(time_t) 60 61 /* Wrapper around strtoul that does not require a cast. */ 62 static unsigned long 63 str_const_to_ul (cch_t * str, cch_t ** ppz, int base) 64 { 65 return strtoul (str, __UNCONST(ppz), base); 66 } 67 68 /* Wrapper around strtol that does not require a cast. */ 69 static long 70 str_const_to_l (cch_t * str, cch_t ** ppz, int base) 71 { 72 return strtol (str, __UNCONST(ppz), base); 73 } 74 75 /* Returns BASE + VAL * SCALE, interpreting BASE = BAD_TIME 76 with errno set as an error situation, and returning BAD_TIME 77 with errno set in an error situation. */ 78 static time_t 79 scale_n_add (time_t base, time_t val, int scale) 80 { 81 if (base == BAD_TIME) 82 { 83 if (errno == 0) 84 errno = EINVAL; 85 return BAD_TIME; 86 } 87 88 if (val > MAX_DURATION / scale) 89 { 90 errno = ERANGE; 91 return BAD_TIME; 92 } 93 94 val *= scale; 95 if (base > MAX_DURATION - val) 96 { 97 errno = ERANGE; 98 return BAD_TIME; 99 } 100 101 return base + val; 102 } 103 104 /* After a number HH has been parsed, parse subsequent :MM or :MM:SS. */ 105 static time_t 106 parse_hr_min_sec (time_t start, cch_t * pz) 107 { 108 int lpct = 0; 109 110 errno = 0; 111 112 /* For as long as our scanner pointer points to a colon *AND* 113 we've not looped before, then keep looping. (two iterations max) */ 114 while ((*pz == ':') && (lpct++ <= 1)) 115 { 116 unsigned long v = str_const_to_ul (pz+1, &pz, 10); 117 118 if (errno != 0) 119 return BAD_TIME; 120 121 start = scale_n_add (v, start, 60); 122 123 if (errno != 0) 124 return BAD_TIME; 125 } 126 127 /* allow for trailing spaces */ 128 while (isspace ((unsigned char)*pz)) 129 pz++; 130 if (*pz != NUL) 131 { 132 errno = EINVAL; 133 return BAD_TIME; 134 } 135 136 return start; 137 } 138 139 /* Parses a value and returns BASE + value * SCALE, interpreting 140 BASE = BAD_TIME with errno set as an error situation, and returning 141 BAD_TIME with errno set in an error situation. */ 142 static time_t 143 parse_scaled_value (time_t base, cch_t ** ppz, cch_t * endp, int scale) 144 { 145 cch_t * pz = *ppz; 146 time_t val; 147 148 if (base == BAD_TIME) 149 return base; 150 151 errno = 0; 152 val = str_const_to_ul (pz, &pz, 10); 153 if (errno != 0) 154 return BAD_TIME; 155 while (isspace ((unsigned char)*pz)) 156 pz++; 157 if (pz != endp) 158 { 159 errno = EINVAL; 160 return BAD_TIME; 161 } 162 163 *ppz = pz; 164 return scale_n_add (base, val, scale); 165 } 166 167 /* Parses the syntax YEAR-MONTH-DAY. 168 PS points into the string, after "YEAR", before "-MONTH-DAY". */ 169 static time_t 170 parse_year_month_day (cch_t * pz, cch_t * ps) 171 { 172 time_t res = 0; 173 174 res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR); 175 176 pz++; /* over the first '-' */ 177 ps = strchr (pz, '-'); 178 if (ps == NULL) 179 { 180 errno = EINVAL; 181 return BAD_TIME; 182 } 183 res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH); 184 185 pz++; /* over the second '-' */ 186 ps = pz + strlen (pz); 187 return parse_scaled_value (res, &pz, ps, SEC_PER_DAY); 188 } 189 190 /* Parses the syntax YYYYMMDD. */ 191 static time_t 192 parse_yearmonthday (cch_t * in_pz) 193 { 194 time_t res = 0; 195 char buf[8]; 196 cch_t * pz; 197 198 if (strlen (in_pz) != 8) 199 { 200 errno = EINVAL; 201 return BAD_TIME; 202 } 203 204 memcpy (buf, in_pz, 4); 205 buf[4] = NUL; 206 pz = buf; 207 res = parse_scaled_value (0, &pz, buf + 4, SEC_PER_YEAR); 208 209 memcpy (buf, in_pz + 4, 2); 210 buf[2] = NUL; 211 pz = buf; 212 res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MONTH); 213 214 memcpy (buf, in_pz + 6, 2); 215 buf[2] = NUL; 216 pz = buf; 217 return parse_scaled_value (res, &pz, buf + 2, SEC_PER_DAY); 218 } 219 220 /* Parses the syntax yy Y mm M ww W dd D. */ 221 static time_t 222 parse_YMWD (cch_t * pz) 223 { 224 time_t res = 0; 225 cch_t * ps = strchr (pz, 'Y'); 226 if (ps != NULL) 227 { 228 res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR); 229 pz++; 230 } 231 232 ps = strchr (pz, 'M'); 233 if (ps != NULL) 234 { 235 res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH); 236 pz++; 237 } 238 239 ps = strchr (pz, 'W'); 240 if (ps != NULL) 241 { 242 res = parse_scaled_value (res, &pz, ps, SEC_PER_WEEK); 243 pz++; 244 } 245 246 ps = strchr (pz, 'D'); 247 if (ps != NULL) 248 { 249 res = parse_scaled_value (res, &pz, ps, SEC_PER_DAY); 250 pz++; 251 } 252 253 while (isspace ((unsigned char)*pz)) 254 pz++; 255 if (*pz != NUL) 256 { 257 errno = EINVAL; 258 return BAD_TIME; 259 } 260 261 return res; 262 } 263 264 /* Parses the syntax HH:MM:SS. 265 PS points into the string, after "HH", before ":MM:SS". */ 266 static time_t 267 parse_hour_minute_second (cch_t * pz, cch_t * ps) 268 { 269 time_t res = 0; 270 271 res = parse_scaled_value (0, &pz, ps, SEC_PER_HR); 272 273 pz++; 274 ps = strchr (pz, ':'); 275 if (ps == NULL) 276 { 277 errno = EINVAL; 278 return BAD_TIME; 279 } 280 281 res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN); 282 283 pz++; 284 ps = pz + strlen (pz); 285 return parse_scaled_value (res, &pz, ps, 1); 286 } 287 288 /* Parses the syntax HHMMSS. */ 289 static time_t 290 parse_hourminutesecond (cch_t * in_pz) 291 { 292 time_t res = 0; 293 char buf[4]; 294 cch_t * pz; 295 296 if (strlen (in_pz) != 6) 297 { 298 errno = EINVAL; 299 return BAD_TIME; 300 } 301 302 memcpy (buf, in_pz, 2); 303 buf[2] = NUL; 304 pz = buf; 305 res = parse_scaled_value (0, &pz, buf + 2, SEC_PER_HR); 306 307 memcpy (buf, in_pz + 2, 2); 308 buf[2] = NUL; 309 pz = buf; 310 res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MIN); 311 312 memcpy (buf, in_pz + 4, 2); 313 buf[2] = NUL; 314 pz = buf; 315 return parse_scaled_value (res, &pz, buf + 2, 1); 316 } 317 318 /* Parses the syntax hh H mm M ss S. */ 319 static time_t 320 parse_HMS (cch_t * pz) 321 { 322 time_t res = 0; 323 cch_t * ps = strchr (pz, 'H'); 324 if (ps != NULL) 325 { 326 res = parse_scaled_value (0, &pz, ps, SEC_PER_HR); 327 pz++; 328 } 329 330 ps = strchr (pz, 'M'); 331 if (ps != NULL) 332 { 333 res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN); 334 pz++; 335 } 336 337 ps = strchr (pz, 'S'); 338 if (ps != NULL) 339 { 340 res = parse_scaled_value (res, &pz, ps, 1); 341 pz++; 342 } 343 344 while (isspace ((unsigned char)*pz)) 345 pz++; 346 if (*pz != NUL) 347 { 348 errno = EINVAL; 349 return BAD_TIME; 350 } 351 352 return res; 353 } 354 355 /* Parses a time (hours, minutes, seconds) specification in either syntax. */ 356 static time_t 357 parse_time (cch_t * pz) 358 { 359 cch_t * ps; 360 time_t res = 0; 361 362 /* 363 * Scan for a hyphen 364 */ 365 ps = strchr (pz, ':'); 366 if (ps != NULL) 367 { 368 res = parse_hour_minute_second (pz, ps); 369 } 370 371 /* 372 * Try for a 'H', 'M' or 'S' suffix 373 */ 374 else if (ps = strpbrk (pz, "HMS"), 375 ps == NULL) 376 { 377 /* Its a YYYYMMDD format: */ 378 res = parse_hourminutesecond (pz); 379 } 380 381 else 382 res = parse_HMS (pz); 383 384 return res; 385 } 386 387 /* Returns a substring of the given string, with spaces at the beginning and at 388 the end destructively removed, per SNOBOL. */ 389 static char * 390 trim (char * pz) 391 { 392 /* trim leading white space */ 393 while (isspace ((unsigned char)*pz)) 394 pz++; 395 396 /* trim trailing white space */ 397 { 398 char * pe = pz + strlen (pz); 399 while ((pe > pz) && isspace ((unsigned char)pe[-1])) 400 pe--; 401 *pe = NUL; 402 } 403 404 return pz; 405 } 406 407 /* 408 * Parse the year/months/days of a time period 409 */ 410 static time_t 411 parse_period (cch_t * in_pz) 412 { 413 char * pT; 414 char * ps; 415 char * pz = strdup (in_pz); 416 void * fptr = pz; 417 time_t res = 0; 418 419 if (pz == NULL) 420 { 421 errno = ENOMEM; 422 return BAD_TIME; 423 } 424 425 pT = strchr (pz, 'T'); 426 if (pT != NULL) 427 { 428 *(pT++) = NUL; 429 pz = trim (pz); 430 pT = trim (pT); 431 } 432 433 /* 434 * Scan for a hyphen 435 */ 436 ps = strchr (pz, '-'); 437 if (ps != NULL) 438 { 439 res = parse_year_month_day (pz, ps); 440 } 441 442 /* 443 * Try for a 'Y', 'M' or 'D' suffix 444 */ 445 else if (ps = strpbrk (pz, "YMWD"), 446 ps == NULL) 447 { 448 /* Its a YYYYMMDD format: */ 449 res = parse_yearmonthday (pz); 450 } 451 452 else 453 res = parse_YMWD (pz); 454 455 if ((errno == 0) && (pT != NULL)) 456 { 457 time_t val = parse_time (pT); 458 res = scale_n_add (res, val, 1); 459 } 460 461 free (fptr); 462 return res; 463 } 464 465 static time_t 466 parse_non_iso8601 (cch_t * pz) 467 { 468 whats_done_t whatd_we_do = NOTHING_IS_DONE; 469 470 time_t res = 0; 471 472 do { 473 time_t val; 474 475 errno = 0; 476 val = str_const_to_l (pz, &pz, 10); 477 if (errno != 0) 478 goto bad_time; 479 480 /* IF we find a colon, then we're going to have a seconds value. 481 We will not loop here any more. We cannot already have parsed 482 a minute value and if we've parsed an hour value, then the result 483 value has to be less than an hour. */ 484 if (*pz == ':') 485 { 486 if (whatd_we_do >= MINUTE_IS_DONE) 487 break; 488 489 val = parse_hr_min_sec (val, pz); 490 491 if ((whatd_we_do == HOUR_IS_DONE) && (val >= SEC_PER_HR)) 492 break; 493 494 return scale_n_add (res, val, 1); 495 } 496 497 { 498 unsigned int mult; 499 500 /* Skip over white space following the number we just parsed. */ 501 while (isspace ((unsigned char)*pz)) 502 pz++; 503 504 switch (*pz) 505 { 506 default: goto bad_time; 507 case NUL: 508 return scale_n_add (res, val, 1); 509 510 case 'y': case 'Y': 511 if (whatd_we_do >= YEAR_IS_DONE) 512 goto bad_time; 513 mult = SEC_PER_YEAR; 514 whatd_we_do = YEAR_IS_DONE; 515 break; 516 517 case 'M': 518 if (whatd_we_do >= MONTH_IS_DONE) 519 goto bad_time; 520 mult = SEC_PER_MONTH; 521 whatd_we_do = MONTH_IS_DONE; 522 break; 523 524 case 'W': 525 if (whatd_we_do >= WEEK_IS_DONE) 526 goto bad_time; 527 mult = SEC_PER_WEEK; 528 whatd_we_do = WEEK_IS_DONE; 529 break; 530 531 case 'd': case 'D': 532 if (whatd_we_do >= DAY_IS_DONE) 533 goto bad_time; 534 mult = SEC_PER_DAY; 535 whatd_we_do = DAY_IS_DONE; 536 break; 537 538 case 'h': 539 if (whatd_we_do >= HOUR_IS_DONE) 540 goto bad_time; 541 mult = SEC_PER_HR; 542 whatd_we_do = HOUR_IS_DONE; 543 break; 544 545 case 'm': 546 if (whatd_we_do >= MINUTE_IS_DONE) 547 goto bad_time; 548 mult = SEC_PER_MIN; 549 whatd_we_do = MINUTE_IS_DONE; 550 break; 551 552 case 's': 553 mult = 1; 554 whatd_we_do = SECOND_IS_DONE; 555 break; 556 } 557 558 res = scale_n_add (res, val, mult); 559 560 pz++; 561 while (isspace ((unsigned char)*pz)) 562 pz++; 563 if (*pz == NUL) 564 return res; 565 566 if (! isdigit ((unsigned char)*pz)) 567 break; 568 } 569 570 } while (whatd_we_do < SECOND_IS_DONE); 571 572 bad_time: 573 errno = EINVAL; 574 return BAD_TIME; 575 } 576 577 time_t 578 parse_duration (char const * pz) 579 { 580 while (isspace ((unsigned char)*pz)) 581 pz++; 582 583 switch (*pz) 584 { 585 case 'P': 586 return parse_period (pz + 1); 587 588 case 'T': 589 return parse_time (pz + 1); 590 591 default: 592 if (isdigit ((unsigned char)*pz)) 593 return parse_non_iso8601 (pz); 594 595 errno = EINVAL; 596 return BAD_TIME; 597 } 598 } 599 600 /* 601 * Local Variables: 602 * mode: C 603 * c-file-style: "gnu" 604 * indent-tabs-mode: nil 605 * End: 606 * end of parse-duration.c */ 607