1 /* $NetBSD: parse-duration.c,v 1.3 2012/02/03 21:36:40 christos Exp $ */ 2 3 /* Parse a time duration and return a seconds count 4 Copyright (C) 2008-2011 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 General Public License as published by 9 the Free Software Foundation; either version 3 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 General Public License for more details. 16 17 You should have received a copy of the GNU 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 inline 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 inline 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 inline 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