mirror of
https://github.com/RetroDECK/Supermodel.git
synced 2024-11-30 01:25:49 +00:00
705 lines
19 KiB
C++
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(§ionIdx, 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(§ionIdx, 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;
|
|
}
|