inifile.cc
1 /***************************************************************************** 2 * Description: inifile.cc 3 * C++ INI file reader 4 * 5 * Derived from a work by Fred Proctor & Will Shackleford 6 * 7 * Author: 8 * License: GPL Version 2 9 * System: Linux 10 * 11 * Copyright (c) 2004 All rights reserved. 12 * 13 * Last change: 14 *****************************************************************************/ 15 16 #include <stdio.h> /* FILE *, fopen(), fclose(), NULL */ 17 #include <stdlib.h> 18 #include <string.h> /* strstr() */ 19 #include <ctype.h> /* isspace() */ 20 #include <fcntl.h> 21 22 23 #include "config.h" 24 #include "inifile.hh" 25 26 #define MAX_EXTEND_LINES 20 27 28 /// Return TRUE if the line has a line-ending problem 29 static bool check_line_endings(const char *s) { 30 if(!s) return false; 31 for(; *s; s++ ) { 32 if(*s == '\r') { 33 char c = s[1]; 34 if(c == '\n' || c == '\0') { 35 static bool warned = 0; 36 if(!warned) 37 fprintf(stderr, "inifile: warning: File contains DOS-style line endings.\n"); 38 warned = true; 39 continue; 40 } 41 fprintf(stderr, "inifile: error: File contains ambiguous carriage returns\n"); 42 return true; 43 } 44 } 45 return false; 46 } 47 48 IniFile::IniFile(int _errMask, FILE *_fp) 49 { 50 fp = _fp; 51 errMask = _errMask; 52 owned = false; 53 54 if(fp != NULL) 55 LockFile(); 56 } 57 58 59 /*! Opens the file for reading. If a file was already open, it is closed 60 and the new one opened. 61 62 @return true on success, false on failure */ 63 bool 64 IniFile::Open(const char *file) 65 { 66 char path[LINELEN] = ""; 67 68 if(IsOpen()) Close(); 69 70 TildeExpansion(file, path, sizeof(path)); 71 72 if((fp = fopen(path, "r")) == NULL) 73 return(false); 74 75 owned = true; 76 77 if(!LockFile()) 78 return(false); 79 80 return(true); 81 } 82 83 84 /*! Closes the file descriptor.. 85 86 @return true on success, false on failure */ 87 bool 88 IniFile::Close() 89 { 90 int rVal = 0; 91 92 if(fp != NULL){ 93 lock.l_type = F_UNLCK; 94 fcntl(fileno(fp), F_SETLKW, &lock); 95 96 if(owned) 97 rVal = fclose(fp); 98 99 fp = NULL; 100 } 101 102 return(rVal == 0); 103 } 104 105 106 IniFile::ErrorCode 107 IniFile::Find(int *result, StrIntPair *pPair, 108 const char *tag, const char *section, int num, int *lineno) 109 { 110 const char *pStr; 111 int tmp; 112 113 if((pStr = Find(tag, section, num)) == NULL){ 114 // We really need an ErrorCode return from Find() and should be passing 115 // in a buffer. Just pick a suitable ErrorCode for now. 116 if (lineno) 117 *lineno = 0; 118 return(ERR_TAG_NOT_FOUND); 119 } 120 121 if(sscanf(pStr, "%i", &tmp) == 1){ 122 *result = tmp; 123 if (lineno) 124 *lineno = lineNo; 125 return(ERR_NONE); 126 } 127 128 while(pPair->pStr != NULL){ 129 if(strcasecmp(pStr, pPair->pStr) == 0){ 130 *result = pPair->value; 131 if (lineno) 132 *lineno = lineNo; 133 return(ERR_NONE); 134 } 135 pPair++; 136 } 137 138 ThrowException(ERR_CONVERSION); 139 return(ERR_CONVERSION); 140 } 141 142 143 IniFile::ErrorCode 144 IniFile::Find(double *result, StrDoublePair *pPair, 145 const char *tag, const char *section, int num, int *lineno) 146 { 147 const char *pStr; 148 double tmp; 149 150 if((pStr = Find(tag, section, num)) == NULL){ 151 // We really need an ErrorCode return from Find() and should be passing 152 // in a buffer. Just pick a suitable ErrorCode for now. 153 if (lineno) 154 *lineno = 0; 155 return(ERR_TAG_NOT_FOUND); 156 } 157 158 if(sscanf(pStr, "%lf", &tmp) == 1){ 159 if (lineno) 160 *lineno = lineNo; 161 *result = tmp; 162 if (lineno) 163 *lineno = lineNo; 164 return(ERR_NONE); 165 } 166 167 while(pPair->pStr != NULL){ 168 if(strcasecmp(pStr, pPair->pStr) == 0){ 169 *result = pPair->value; 170 if (lineno) 171 *lineno = lineNo; 172 return(ERR_NONE); 173 } 174 pPair++; 175 } 176 177 ThrowException(ERR_CONVERSION); 178 return(ERR_CONVERSION); 179 } 180 181 182 /*! Finds the nth tag in section. 183 184 @param tag Entry in the ini file to find. 185 186 @param section The section to look for the tag. 187 188 @param num (optionally) the Nth occurrence of the tag. 189 190 @return pointer to the the variable after the '=' delimiter */ 191 const char * 192 IniFile::Find(const char *_tag, const char *_section, int _num, int *lineno) 193 { 194 // WTF, return a pointer to the middle of a local buffer? 195 // FIX: this is totally non-reentrant. 196 static char line[LINELEN + 2] = ""; /* 1 for newline, 1 for NULL */ 197 char bracketSection[LINELEN + 2] = ""; 198 char *nonWhite; 199 int newLinePos; /* position of newline to strip */ 200 int len; 201 char tagEnd; 202 char *valueString; 203 char *endValueString; 204 205 char eline [(LINELEN + 2) * (MAX_EXTEND_LINES + 1)]; 206 char* elineptr; 207 char* elinenext; 208 int extend_ct = 0; 209 210 // For exceptions. 211 lineNo = 0; 212 tag = _tag; 213 section = _section; 214 num = _num; 215 216 /* check valid file */ 217 if(!CheckIfOpen()) 218 return(NULL); 219 220 /* start from beginning */ 221 rewind(fp); 222 223 /* check for section first-- if it's non-NULL, then position file at 224 line after [section] */ 225 if(section != NULL){ 226 sprintf(bracketSection, "[%s]", section); 227 228 /* find [section], and position fp just after it */ 229 while (!feof(fp)) { 230 231 if (NULL == fgets(line, LINELEN + 1, fp)) { 232 /* got to end of file without finding it */ 233 ThrowException(ERR_SECTION_NOT_FOUND); 234 return(NULL); 235 } 236 237 if(check_line_endings(line)) { 238 ThrowException(ERR_CONVERSION); 239 return(NULL); 240 } 241 242 /* got a line */ 243 lineNo++; 244 245 /* strip off newline */ 246 newLinePos = strlen(line) - 1; /* newline is on back from 0 */ 247 if (newLinePos < 0) { 248 newLinePos = 0; 249 } 250 if (line[newLinePos] == '\n') { 251 line[newLinePos] = 0; /* make the newline 0 */ 252 } 253 254 if (NULL == (nonWhite = SkipWhite(line))) { 255 /* blank line-- skip */ 256 continue; 257 } 258 259 /* not a blank line, and nonwhite is first char */ 260 if (strncmp(bracketSection, nonWhite, strlen(bracketSection)) != 0){ 261 /* not on this line */ 262 continue; 263 } 264 265 /* it matches-- fp is now set up for search on tag */ 266 break; 267 } 268 } 269 270 while (!feof(fp)) { 271 /* check for end of file */ 272 if (NULL == fgets(line, LINELEN + 1, (FILE *) fp)) { 273 /* got to end of file without finding it */ 274 ThrowException(ERR_TAG_NOT_FOUND); 275 return(NULL); 276 } 277 278 if(check_line_endings(line)) { 279 ThrowException(ERR_CONVERSION); 280 return(NULL); 281 } 282 283 /* got a line */ 284 lineNo++; 285 286 /* strip off newline */ 287 newLinePos = strlen(line) - 1; /* newline is on back from 0 */ 288 if (newLinePos < 0) { 289 newLinePos = 0; 290 } 291 if (line[newLinePos] == '\n') { 292 line[newLinePos] = 0; /* make the newline 0 */ 293 } 294 // honor backslash (\) as line-end escape 295 if (newLinePos > 0 && line[newLinePos-1] == '\\') { 296 newLinePos = newLinePos-1; 297 line[newLinePos] = 0; 298 if (!extend_ct) { 299 elineptr = (char*)eline; //first time 300 strncpy(elineptr,line,newLinePos); 301 elinenext = elineptr + newLinePos; 302 } else { 303 strncpy(elinenext,line,newLinePos); 304 elinenext = elinenext + newLinePos; 305 } 306 *elinenext = 0; 307 extend_ct++; 308 if (extend_ct > MAX_EXTEND_LINES) { 309 fprintf(stderr, 310 "INIFILE lineno=%d:Too many backslash line extends (limit=%d)\n", 311 lineNo, MAX_EXTEND_LINES); 312 ThrowException(ERR_OVER_EXTENDED); 313 return(NULL); 314 } 315 continue; // get next line to extend 316 } else { 317 if (extend_ct) { 318 strncpy(elinenext,line,newLinePos); 319 elinenext = elinenext + newLinePos; 320 *elinenext = 0; 321 } 322 } 323 if (!extend_ct) { 324 elineptr = (char*)line; 325 } 326 extend_ct = 0; 327 328 /* skip leading whitespace */ 329 if (NULL == (nonWhite = SkipWhite(elineptr))) { 330 /* blank line-- skip */ 331 continue; 332 } 333 334 /* check for '[' char-- if so, it's a section tag, and we're out 335 of our section */ 336 if (NULL != section && nonWhite[0] == '[') { 337 ThrowException(ERR_TAG_NOT_FOUND); 338 return(NULL); 339 } 340 341 len = strlen(tag); 342 if (strncmp(tag, nonWhite, len) != 0) { 343 /* not on this line */ 344 continue; 345 } 346 347 /* it matches the first part of the string-- if whitespace or = is 348 next char then call it a match */ 349 tagEnd = nonWhite[len]; 350 if (tagEnd == ' ' || tagEnd == '\r' || tagEnd == '\t' 351 || tagEnd == '\n' || tagEnd == '=') { 352 /* it matches-- return string after =, or NULL */ 353 if (--_num > 0) { 354 /* Not looking for this one, so skip it... */ 355 continue; 356 } 357 nonWhite += len; 358 valueString = AfterEqual(nonWhite); 359 /* Eliminate white space at the end of a line also. */ 360 if (NULL == valueString) { 361 ThrowException(ERR_TAG_NOT_FOUND); 362 return(NULL); 363 } 364 endValueString = valueString + strlen(valueString) - 1; 365 while (*endValueString == ' ' || *endValueString == '\t' 366 || *endValueString == '\r') { 367 *endValueString = 0; 368 endValueString--; 369 } 370 if (lineno) 371 *lineno = lineNo; 372 return(valueString); 373 } 374 /* else continue */ 375 } 376 377 ThrowException(ERR_TAG_NOT_FOUND); 378 return(NULL); 379 } 380 381 const char * 382 IniFile::FindString(char *dest, size_t n, const char *_tag, const char *_section, int _num, int *lineno) 383 { 384 const char *res = Find(_tag, _section, _num, lineno); 385 if(res == NULL) 386 return res; 387 int r = snprintf(dest, n, "%s", res); 388 if(r < 0 || (size_t)r >= n) { 389 ThrowException(ERR_CONVERSION); 390 return NULL; 391 } 392 return dest; 393 } 394 395 const char * 396 IniFile::FindPath(char *dest, size_t n, const char *_tag, const char *_section, int _num, int *lineno) 397 { 398 const char *res = Find(_tag, _section, _num, lineno); 399 if(!res) 400 return res; 401 if(TildeExpansion(res, dest, n)) { 402 return 0; 403 } 404 return dest; 405 } 406 407 bool 408 IniFile::CheckIfOpen(void) 409 { 410 if(IsOpen()) 411 return(true); 412 413 ThrowException(ERR_NOT_OPEN); 414 415 return(false); 416 } 417 418 419 bool 420 IniFile::LockFile(void) 421 { 422 lock.l_type = F_RDLCK; 423 lock.l_whence = SEEK_SET; 424 lock.l_start = 0; 425 lock.l_len = 0; 426 427 if(fcntl(fileno(fp), F_SETLK, &lock) == -1){ 428 if(owned) 429 fclose(fp); 430 431 fp = NULL; 432 return(false); 433 } 434 435 return(true); 436 } 437 438 439 /*! Expands the tilde to $(HOME) and concatenates file to it. If the first char 440 If file does not start with ~/, file will be copied into path as-is. 441 442 @param the input filename 443 444 @param pointer for returning the resulting expanded name 445 446 */ 447 IniFile::ErrorCode 448 IniFile::TildeExpansion(const char *file, char *path, size_t size) 449 { 450 char *home; 451 452 int res = snprintf(path, size, "%s", file); 453 if(res < 0 || (size_t)res >= size) 454 return ERR_CONVERSION; 455 456 if (strlen(file) < 2 || !(file[0] == '~' && file[1] == '/')) { 457 /* no tilde expansion required, or unsupported 458 tilde expansion type requested */ 459 return ERR_NONE; 460 } 461 462 home = getenv("HOME"); 463 if (!home) { 464 ThrowException(ERR_CONVERSION); 465 return ERR_CONVERSION; 466 } 467 468 res = snprintf(path, size, "%s%s", home, file + 1); 469 if(res < 0 || (size_t)res >= size) { 470 ThrowException(ERR_CONVERSION); 471 return ERR_CONVERSION; 472 } 473 474 return ERR_NONE; 475 } 476 477 int 478 TildeExpansion(const char *file, char *path, size_t size) 479 { 480 static IniFile f; 481 return !f.TildeExpansion(file, path, size); 482 } 483 484 void 485 IniFile::ThrowException(ErrorCode errCode) 486 { 487 if(errCode & errMask){ 488 exception.errCode = errCode; 489 exception.tag = tag; 490 exception.section = section; 491 exception.num = num; 492 exception.lineNo = lineNo; 493 throw(exception); 494 } 495 } 496 497 498 /*! Ignoring any tabs, spaces or other white spaces, finds the first 499 character after the '=' delimiter. 500 501 @param string Pointer to the tag 502 503 @return NULL or pointer to first non-white char after the delimiter 504 505 Called By: find() and section() only. */ 506 char * 507 IniFile::AfterEqual(const char *string) 508 { 509 const char *spot = string; 510 511 for (;;) { 512 if (*spot == '=') { 513 /* = is there-- return next non-white, or NULL if not there */ 514 for (;;) { 515 spot++; 516 if (0 == *spot) { 517 /* ran out */ 518 return(NULL); 519 } else if (*spot != ' ' && *spot != '\t' && *spot != '\r' 520 && *spot != '\n') { 521 /* matched! */ 522 return((char *)spot); 523 } else { 524 /* keep going for the text */ 525 continue; 526 } 527 } 528 } else if (*spot == 0) { 529 /* end of string */ 530 return(NULL); 531 } else { 532 /* haven't seen '=' yet-- keep going */ 533 spot++; 534 continue; 535 } 536 } 537 } 538 539 540 /*! Finds the first non-white character on a new line and returns a 541 pointer. Ignores any line that starts with a comment char i.e. a ';' or 542 '#'. 543 544 @return NULL if not found or a valid pointer. 545 546 Called By: find() and section() only. */ 547 char * 548 IniFile::SkipWhite(const char *string) 549 { 550 while(true){ 551 if (*string == 0) { 552 return(NULL); 553 } 554 555 if ((*string == ';') || (*string == '#')) { 556 return(NULL); 557 } 558 559 if (*string != ' ' && *string != '\t' && *string != '\r' 560 && *string != '\n') { 561 return((char *)string); 562 } 563 564 string++; 565 } 566 } 567 568 569 void 570 IniFile::Exception::Print(FILE *fp) 571 { 572 const char *msg; 573 574 switch(errCode){ 575 case ERR_NONE: 576 msg = "ERR_NONE"; 577 break; 578 579 case ERR_NOT_OPEN: 580 msg = "ERR_NOT_OPEN"; 581 break; 582 583 case ERR_SECTION_NOT_FOUND: 584 msg = "ERR_SECTION_NOT_FOUND"; 585 break; 586 587 case ERR_TAG_NOT_FOUND: 588 msg = "ERR_TAG_NOT_FOUND"; 589 break; 590 591 case ERR_CONVERSION: 592 msg = "ERR_CONVERSION"; 593 break; 594 595 case ERR_LIMITS: 596 msg = "ERR_LIMITS"; 597 break; 598 599 case ERR_OVER_EXTENDED: 600 msg = "ERR_OVER_EXTENDED"; 601 break; 602 603 default: 604 msg = "UNKNOWN"; 605 } 606 607 fprintf(fp, "INIFILE: %s, section=%s, tag=%s, num=%d, lineNo=%d\n", 608 msg, section, tag, num, lineNo); 609 } 610 611 612 extern "C" const char * 613 iniFind(FILE *fp, const char *tag, const char *section) 614 { 615 IniFile f(false, fp); 616 617 return(f.Find(tag, section)); 618 } 619 620 extern "C" const int 621 iniFindInt(FILE *fp, const char *tag, const char *section, int *result) 622 { 623 IniFile f(false, fp); 624 return(f.Find(result, tag, section)); 625 } 626 627 extern "C" const int 628 iniFindDouble(FILE *fp, const char *tag, const char *section, double *result) 629 { 630 IniFile f(false, fp); 631 return(f.Find(result, tag, section)); 632 }