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