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