Supermodel/Src/INIFile.cpp

705 lines
19 KiB
C++

/**
** Supermodel
** A Sega Model 3 Arcade Emulator.
** Copyright 2011 Bart Trzynadlowski, Nik Henson
**
** This file is part of Supermodel.
**
** Supermodel is free software: you can redistribute it and/or modify it under
** the terms of the GNU General Public License as published by the Free
** Software Foundation, either version 3 of the License, or (at your option)
** any later version.
**
** Supermodel is distributed in the hope that it will be useful, but WITHOUT
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
** FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
** more details.
**
** You should have received a copy of the GNU General Public License along
** with Supermodel. If not, see <http://www.gnu.org/licenses/>.
**/
/*
* INIFile.cpp
*
* INI file management. Implementation of the CINIFile class.
*
* To-Do List
* ----------
* - Add an iterator to retrieve all settings associated with a given section.
* This will allow detection of invalid setting names, if the caller desires.
* - Add boolean on/off, true/false keywords.
* - Note that linePtr does not necessarily correspond to actual lines in the
* file (newlines should be counted by the tokenizer for that).
*
* Grammar
* -------
*
* Section: '[' Identifier ']'
* Line: Identifier '=' Argument
* Argument: Number
* String
*
* Overview
* --------
* INI files are linear in nature, divided into sections. Each section has
* settings associated with it which may be either numerical values or strings.
* Presently, only unsigned 32-bit integers are supported.
*
* INI files are first opened, then parsed, and finally closed. The parse tree
* is cleared only during object construction and when the INI file is closed.
* Default settings may be inserted before a file is opened or parsed. When an
* INI file is written out, the current INI file on the disk is cleared. If an
* error occurs during this process, the data will be lost.
*
* Sections are added only when a setting is found in that section. Empty
* sections are not added to the tree with the exception of the default ("")
* section. A default section name can be specified, which creates an alias
* for the default section. The alias will be output when the file is
* written (there will be no settings without an explicit section).
*/
#include "Supermodel.h"
using namespace std;
/******************************************************************************
Basic Functions
******************************************************************************/
bool CINIFile::Write(const char *comment)
{
bool writeSuccess;
// In order to truncate, we must close and reopen as truncated
File.close();
File.clear(); // required to clear EOF flag (open() does not clear)
File.open(FileName.c_str(),fstream::out|fstream::trunc);
if (File.fail())
{
return FAIL;
}
// Output comment
if (comment != NULL)
File << comment << endl;
// Iterate through all sections sequentially
for (unsigned i = 0; i < Sections.size(); i++)
{
if (Sections[i].Settings.size() != 0)
{
// Output section name
if (Sections[i].Name != "")
File << "[ " << Sections[i].Name << " ]" << endl << endl;
else // if null name, use default section name (if exists) or nothing at all
{
if (DefaultSectionName != "")
File << "[ " << DefaultSectionName << " ]" << endl << endl;
}
// Iterate through all settings within this section
for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
{
// Output setting
File << Sections[i].Settings[j].Name << " = ";
if (Sections[i].Settings[j].isNumber)
File << Sections[i].Settings[j].value << endl;
else
File << '\"' << Sections[i].Settings[j].String << '\"' << endl;
}
// New line
File << endl;
}
}
writeSuccess = File.good()?OKAY:FAIL;
// Close and reopen as read/write
File.close();
File.open(FileName.c_str(),fstream::in|fstream::out);
if (File.fail())
{
//printf("unable to re-open %s for reading/writing\n", FileName.c_str());
return FAIL;
}
// Report any errors that occurred during writing
return writeSuccess;
}
bool CINIFile::Open(const char *fileNameStr)
{
FileName = fileNameStr;
// Try to open for reading AND writing
File.open(fileNameStr, fstream::in|fstream::out);
if (File.fail())
return FAIL;
InitParseTree();
return OKAY;
}
bool CINIFile::OpenAndCreate(const char *fileNameStr)
{
FileName = fileNameStr;
// Try to open for reading and writing
File.open(fileNameStr, fstream::in|fstream::out);
if (File.fail())
{
// File does not exist, try opening as write only (create it)
File.clear();
File.open(fileNameStr, fstream::out);
if (File.fail())
return FAIL;
}
InitParseTree();
return OKAY;
}
void CINIFile::Close(void)
{
File.close();
Sections.clear(); // clear the parse tree!
}
/******************************************************************************
Management of Settings
******************************************************************************/
// Finds index of first matching section in the section list. Returns FAIL if not found.
bool CINIFile::LookUpSection(unsigned *idx, string SectionName)
{
for (unsigned i = 0; i < Sections.size(); i++)
{
if ((Sections[i].Name == SectionName) ||
((Sections[i].Name == "") && (SectionName == DefaultSectionName)) || // if default section, also accept its alias
((Sections[i].Name == DefaultSectionName) && (SectionName == ""))) // ...
{
*idx = i;
return OKAY;
}
}
return FAIL;
}
// Assigns a value to the given setting, creating the setting if it does not exist. Nulls out the string (sets it to "").
void CINIFile::Set(string SectionName, string SettingName, int value)
{
struct Setting NewSetting;
unsigned sectionIdx, settingIdx;
// Check if the section exists anywhere in parse tree. If not, create it
if (OKAY != LookUpSection(&sectionIdx, SectionName))
{
//printf("unable to find %s:%s, creating section\n", SectionName.c_str(), SettingName.c_str());
struct Section NewSection;
NewSection.Name = SectionName;
Sections.push_back(NewSection);
sectionIdx = Sections.size()-1; // the new section will be at the last index
}
// Search through all sections with the requested name for the first occurance of the desired setting
for (unsigned i = 0; i < Sections.size(); i++)
{
if ((Sections[i].Name == SectionName) ||
((Sections[i].Name == "") && (SectionName == DefaultSectionName)) || // accept alias for default section
((Sections[i].Name == DefaultSectionName) && (SectionName == ""))) // ...
{
for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
{
if (Sections[i].Settings[j].Name == SettingName)
{
// Found it! Update value of this setting
sectionIdx = i;
settingIdx = j;
goto UpdateIntValue;
}
}
}
}
// Couldn't find setting, create it in the first matching section found earlier
NewSetting.Name = SettingName;
Sections[sectionIdx].Settings.push_back(NewSetting);
settingIdx = Sections[sectionIdx].Settings.size()-1;
// Update the setting!
UpdateIntValue:
Sections[sectionIdx].Settings[settingIdx].isNumber = true;
Sections[sectionIdx].Settings[settingIdx].value = value;
Sections[sectionIdx].Settings[settingIdx].String = "";
}
// Assigns the string to the given setting, creating the setting if it does not exist. Zeros out the value.
void CINIFile::Set(string SectionName, string SettingName, string String)
{
struct Setting NewSetting;
unsigned sectionIdx, settingIdx;
// Check if the section exists anywhere in parse tree. If not, create it
if (OKAY != LookUpSection(&sectionIdx, SectionName))
{
//printf("unable to find %s:%s, creating section\n", SectionName.c_str(), SettingName.c_str());
struct Section NewSection;
NewSection.Name = SectionName;
Sections.push_back(NewSection);
sectionIdx = Sections.size()-1; // the new section will be at the last index
}
// Search through all sections with the requested name for the first occurance of the desired setting
for (unsigned i = 0; i < Sections.size(); i++)
{
if ((Sections[i].Name == SectionName) ||
((Sections[i].Name == "") && (SectionName == DefaultSectionName)) || // accept alias for default section
((Sections[i].Name == DefaultSectionName) && (SectionName == ""))) // ...
{
for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
{
if (Sections[i].Settings[j].Name == SettingName)
{
// Found it! Update value of this setting
sectionIdx = i;
settingIdx = j;
goto UpdateString;
}
}
}
}
// Couldn't find setting, create it in the first matching section found earlier
NewSetting.Name = SettingName;
Sections[sectionIdx].Settings.push_back(NewSetting);
settingIdx = Sections[sectionIdx].Settings.size()-1;
// Update the setting!
UpdateString:
Sections[sectionIdx].Settings[settingIdx].isNumber = false;
Sections[sectionIdx].Settings[settingIdx].String = String;
Sections[sectionIdx].Settings[settingIdx].value = 0;
}
// Obtains a numerical setting, if it exists, otherwise does nothing.
bool CINIFile::Get(string SectionName, string SettingName, int& value)
{
for (unsigned i = 0; i < Sections.size(); i++)
{
if ((Sections[i].Name == SectionName) ||
((Sections[i].Name == "") && (SectionName == DefaultSectionName)) || // accept alias for default section
((Sections[i].Name == DefaultSectionName) && (SectionName == ""))) // ...
{
for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
{
if (Sections[i].Settings[j].Name == SettingName)
{
value = Sections[i].Settings[j].value;
return OKAY;
}
}
}
}
return FAIL;
}
bool CINIFile::Get(string SectionName, string SettingName, unsigned& value)
{
int intVal;
if (Get(SectionName, SettingName, intVal) == FAIL || intVal < 0)
return FAIL;
value = (unsigned)intVal;
return OKAY;
}
bool CINIFile::Get(string SectionName, string SettingName, bool& value)
{
int intVal;
if (Get(SectionName, SettingName, intVal) == FAIL)
return FAIL;
value = intVal > 0;
return OKAY;
}
// Obtains a string setting, if it exists, otherwise does nothing.
bool CINIFile::Get(string SectionName, string SettingName, string& String)
{
for (unsigned i = 0; i < Sections.size(); i++)
{
if ((Sections[i].Name == SectionName) ||
((Sections[i].Name == "") && (SectionName == DefaultSectionName)) || // accept alias for default section
((Sections[i].Name == DefaultSectionName) && (SectionName == ""))) // ...
{
for (unsigned j = 0; j < Sections[i].Settings.size(); j++)
{
if (Sections[i].Settings[j].Name == SettingName)
{
String = Sections[i].Settings[j].String;
return OKAY;
}
}
}
}
return FAIL;
}
void CINIFile::SetDefaultSectionName(string SectionName)
{
DefaultSectionName = SectionName;
}
/******************************************************************************
Tokenizer
Never need to check for newlines because it is assumed these have been
stripped by getline().
******************************************************************************/
// Token types
#define TOKEN_INVALID -1
#define TOKEN_NULL 0
#define TOKEN_IDENTIFIER 1
#define TOKEN_NUMBER 2
#define TOKEN_STRING 3
// Token constructor (initializes token to null)
CINIFile::CToken::CToken(void)
{
type = TOKEN_NULL;
}
// Returns true for white space, comment symbol, or null terminator.
static bool IsBlank(char c)
{
if (isspace(c) || (c==';') || (c=='\0'))
return true;
return false;
}
// Fetches a string. Tolerates all characters between quotes, except \n.
CINIFile::CToken CINIFile::GetString(void)
{
CToken T;
T.type = TOKEN_STRING;
// Search for next quote
++linePtr;
while (1)
{
if (linePtr[0] == '\"')
{
++linePtr; // so we can find next token
break;
}
else if ((linePtr[0] == '\0') || (linePtr[0] == '\n'))
{
//printf("tokenizer: warning: string is missing end quote\n");
break;
}
else
T.String += linePtr[0];
++linePtr;
}
return T;
}
//static bool IsOverflown(unsigned long long number, bool isNeg)
//{
// return (!isNeg && number > 0x000000007FFFFFFFULL) || (isNeg && number > 0x0000000080000000ULL);
//}
// Fetch number (decimal or hexadecimal positive/negative integer).
// linePtr must point to a character and therefore linePtr[1] is guaranteed to be within bounds.
CINIFile::CToken CINIFile::GetNumber(void)
{
CToken T;
unsigned long long number = 0;
bool isNeg = false;
//int overflow = 0;
T.type = TOKEN_NUMBER;
// See if begins with minus sign
if (linePtr[0]=='-')
{
isNeg = true;
linePtr++;
}
// Hexadecimal?
if ((linePtr[0]=='0') && ((linePtr[1]=='X') || (linePtr[1]=='x')))
{
linePtr += 2; // advance to digits
// Ensure that at we have at least one digit
if (!isxdigit(linePtr[0]))
{
//printf("tokenizer: invalid hexadecimal number\n");
T.type = TOKEN_INVALID;
return T;
}
// Read number digit by digit
while (1)
{
if (isxdigit(linePtr[0]))
{
number <<= 4;
if (isdigit(linePtr[0]))
number |= (linePtr[0]-'0');
else if (isupper(linePtr[0]))
number |= (linePtr[0]-'A');
else // must be lowercase...
number |= (linePtr[0]-'a');
++linePtr;
// Check for overflows
//if (IsOverflown(number, isNeg))
// overflow = 1;
}
else if (IsBlank(linePtr[0]))
break;
else
{
//printf("tokenizer: invalid hexadecimal number\n");
T.type = TOKEN_INVALID;
return T;
}
}
}
// Decimal?
else
{
// Read number digit by digit
while (1)
{
if (isdigit(linePtr[0]))
{
number *= 10;
number += (linePtr[0]-'0');
++linePtr;
// Check for overflows
//if (IsOverflown(number, isNeg))
// overflow = 1;
}
else if (IsBlank(linePtr[0]))
break;
else
{
//printf("tokenizer: invalid number\n");
T.type = TOKEN_INVALID;
return T;
}
}
}
//if (overflow)
// printf("tokenizer: number exceeds 32 bits and has been truncated\n");
T.number = (isNeg ? -(int)number : (int)number);
return T;
}
// Fetch identifier
CINIFile::CToken CINIFile::GetIdentifier(void)
{
CToken T;
T.type = TOKEN_IDENTIFIER;
while (1)
{
if (isalpha(linePtr[0]) || isdigit(linePtr[0]) || (linePtr[0]=='_'))
{
T.String += linePtr[0];
++linePtr;
}
else
break;
}
return T;
}
// Fetch token
CINIFile::CToken CINIFile::GetToken(void)
{
CToken T;
while (1)
{
// Gobble up whitespace
if (isspace(linePtr[0]))
++linePtr;
// Comment or end of line
else if ((linePtr[0]==';') || (linePtr[0]=='\0'))
{
T.type = TOKEN_NULL;
return T; // do not advance linePtr (so we do not de-sync the parser)
}
// Delimiters
else if ((linePtr[0]=='[') || (linePtr[0]==']') || (linePtr[0]=='='))
{
T.type = *linePtr++;
return T;
}
// Identifier?
else if (isalpha(linePtr[0]) || (linePtr[0] == '_'))
{
T = GetIdentifier();
return T;
}
// Number? (+/-?)
else if (linePtr[0]=='-' || isdigit(linePtr[0]))
{
T = GetNumber();
return T;
}
// String?
else if (linePtr[0]=='\"')
{
T = GetString();
return T;
}
// Illegal symbol
else
break; // return null token
}
// If we got here, invalid token
T.type = TOKEN_INVALID;
return T;
}
/******************************************************************************
Parser
******************************************************************************/
/*
* Parse tree is initialized with a blank section in case the user adds
* settings with defined sections before adding settings without an explicit
* section. If this is not done, settings without a section will be wrongly
* output as part of the previous section in the parse tree.
*/
void CINIFile::InitParseTree(void)
{
struct Section FirstSection;
FirstSection.Name = "";
Sections.clear();
Sections.push_back(FirstSection);
}
bool CINIFile::Parse(void)
{
CToken T, U, V, W;
string currentSection; // current section we're processing
bool parseStatus = OKAY;
lineNum = 0;
if (!File.is_open())
return FAIL;
File.clear();
if (!File.good())
return FAIL;
while (!File.eof())
{
++lineNum;
File.getline(lineBuf,2048);
if (File.fail())
return FAIL;
linePtr = lineBuf; // beginning of line
// Top level
T = GetToken();
U = GetToken();
V = GetToken();
W = GetToken(); // should always be null
switch (T.type)
{
// [ Identifier ]
case '[':
if ((U.type==TOKEN_IDENTIFIER) && (V.type==']') && (W.type==TOKEN_NULL))
{
//printf("Section: %s\n", U.String.c_str());
currentSection = U.String;
}
else
{
parseStatus = FAIL;
//printf("%d: parse error\n", lineNum);
}
break;
// Identifier '=' Argument
case TOKEN_IDENTIFIER:
if (U.type != '=')
{
parseStatus = FAIL;
//printf("%d: expected '=' after identifier\n", lineNum);
}
else
{
if (((V.type==TOKEN_NUMBER) || (V.type==TOKEN_STRING)) && (W.type==TOKEN_NULL))
{
if (V.type == TOKEN_NUMBER)
{
//printf("\t%s = %X\n", T.String.c_str(), V.number);
Set(currentSection, T.String, V.number);
}
else if (V.type == TOKEN_STRING)
{
//printf("\t%s = %s\n", T.String.c_str(), V.String.c_str());
Set(currentSection, T.String, V.String);
}
}
else
{
parseStatus = FAIL;
//printf("%d: expected a number or string after '='\n", lineNum);
}
}
break;
// Blank line
case TOKEN_NULL:
break;
// Illegal
default:
parseStatus = FAIL;
//printf("%d: parse error\n", lineNum);
break;
}
}
//printf("end of file reached\n");
return parseStatus;
}