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