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