Files
Project64-Legacy/FileHandler.cpp
rgarciaz80 c93b092a62 Entries should now be saved in order in the example of
Cheat10 vs Cheat2_ (The _ technically comes after the 0 but not if the value 10 is compared to 2, although the fix does not compare value in this way)
Name was added as an important "header" for entries
So, Good Name, Internal Name, and Name now get moved above to right below the [XXXXXXX:XXXXXXX-C:XX] heading
2021-03-13 10:26:47 -06:00

510 lines
15 KiB
C++

#include "FileHandler.h"
#include "Settings Common Defines.h"
#include <iostream>
#include <fstream>
#include <ctime>
#include <Windows.h>
#include <algorithm>
#include <sys/stat.h>
#include <regex>
using namespace std;
////////////////////////
// GLOBAL VARIABLES!!!
////////////////////////
vector<FileStuff> myFiles;
// Used for atomic access to myFiles
HANDLE gLVMutex = CreateMutex(NULL, FALSE, NULL);
// WRITE_ENTRY is a normal write to file, this will remove any duplicate settings
// DELETE_SETTING is a removal of the setting and (optional) value pair
// DELETE_ENTRY will remove the entire entry from it's [id] and including all setting/value pairs underneath it
enum supported_writes {WRITE_ENTRY, DELETE_SETTING, DELETE_ENTRY};
// The function prototypes (Used to communicate with the class)
FileStuff *GetTheFile(char *filename);
string ReadHandler(char *filename, char *id, char *setting, char *def, BOOLEAN getting_value);
void WriteHandler(char *filename, char *id, char *setting, char *value, supported_writes write_type);
////////////////////////////////
// CLASS IMPLEMENTATION (PUBLIC)
////////////////////////////////
FileStuff::FileStuff() {
this->buffer.clear();
this->entry_start = this->buffer.end();
this->entry_end = this->buffer.end();
this->filename = "";
this->fullpath = "";
this->last_offset = 0;
}
FileStuff::FileStuff(char *filename) {
this->buffer.clear();
this->entry_start = this->buffer.end();
this->entry_end = this->buffer.end();
this->SetFileName(filename);
this->last_offset = 0;
}
FileStuff::~FileStuff() {
this->buffer.clear();
this->buffer.shrink_to_fit();
}
void FileStuff::SetFileName(char *filename) {
char path[MAX_PATH];
string::size_type pos;
this->filename = filename;
// Create the full path to be used to open the file
GetModuleFileName(NULL, path, MAX_PATH);
pos = string(path).find_last_of("\\/");
this->fullpath = string(path).substr(0, pos) + "\\Config\\" + filename;
}
string FileStuff::GetFileName() {
return this->filename;
}
void FileStuff::SortEntry(string search) {
vector<string>::iterator found;
const static string s[] = {"Good Name=", "Internal Name=", "Name="};
// regex to exactly match a game ID [00000000-00000000-C:00]
// Comments are optional at the end but they can be read as well
regex expr ("^\\[([A-Fa-f0-9]){8}-([A-Fa-f0-9]){8}-C:([A-Fa-f0-9]){2}\\]\\s*(?://?.*?\\s*)?$");
this->FindEntry(search);
// The entry is empty
if (this->entry_start == this->buffer.end() || this->entry_end == this->buffer.begin())
return;
// Only prioritize Good Name, Internal Name, and Name if this is a game entry
// The format is [xxxxxxxx-xxxxxxxx-C:xx] where x are hex characters
if (regex_match(*this->entry_start, expr)) {
for (size_t i = 0; i < sizeof(s) / sizeof(s[0]); i++) {
found = find_if(this->entry_start, this->entry_end, [i](string p) { return p.compare(0, s[i].length(), s[i]) == 0;});
if (found != this->entry_end) {
++this->entry_start;
std::swap(*this->entry_start, *found);
}
}
}
// Ignore white space and comments at the bottom, it will end up at the top of the list after a sort
while(isprint((*(this->entry_end - 1))[0]) == 0 || ((*(this->entry_end - 1))[0] == '/' && (*(this->entry_end - 1))[1] == '/'))
--this->entry_end;
// This sort does not currently handle spaces or comments, if they are in new lines they will be moved to the top
std::sort(this->entry_start + 1, this->entry_end,
[](const string& a, const string& b) {
for (int count = 0; count < a.length() && count < b.length(); count++) {
// The same character, keep on checking
if (a[count] == b[count])
continue;
// Check the next character (if available)
// This is to prevent Cheat10= coming before Cheat2=
if (count + 1 < a.length() && count + 1 < b.length())
if ((a[count + 1] == '=' || a[count + 1] == '_') && b[count + 1] >= '0' && b[count + 1] <= '9')
return true;
return a[count] < b[count];
}
// These two are the same so it doesn't matter, just say a is less than
return true;
});
}
string FileStuff::GetValue(char *id, char *setting, char *def, bool fetch) {
string search, value;
int len;
size_t find;
vector<string>::iterator found;
// The search string
search = "[" + (string)id + "]";
len = setting == NULL ? 0 : strlen(setting);
// Find the start of the section we are interested in and the end
this->FindEntry(search);
if (fetch) {
// Search for setting=
found = find_if(this->entry_start, this->entry_end, [setting, len](string p) { return p.compare(0, len + 1, (string)setting + "=") == 0;});
if (found != this->entry_end) {
// Make sure that comments are ignored
find = (*found).find("//");
if (find != string::npos)
value = (*found).substr(len + 1, find - len + 1);
else
value = (*found).substr(len + 1, find);
// Trim whitespace off the end
for (int i = value.length() - 1; i >= 0; i--) {
if(isspace(value[i]))
value[i] = '\0';
else
break;
}
}
}
// No = but this may be an entry alone (For example ExpansionPak // Use expansion)
else {
found = find_if(this->entry_start, this->entry_end, [setting, len](string p) { return p.compare(0, len, setting) == 0; });
if (found != this->entry_end)
value = string(setting);
}
if (value.empty())
value = (def == NULL) ? "" : string(def);
return value;
}
string FileStuff::GetKeys(char *id) {
string result, hold;
size_t loc;
this->FindEntry("[" + string(id) +"]");
for (vector<string>::iterator it = this->entry_start; it < this->entry_end; ++it) {
hold = (*it);
hold.erase(remove(hold.begin(), hold.end(), '\r'), hold.end());
hold.erase(remove(hold.begin(), hold.end(), '\n'), hold.end());
// Skip empty lines, the first entry that contains the header, and comments
if (hold.length() == 0 || hold[0] == '[' || hold.compare(0, 2, "//") == 0)
continue;
loc = hold.find("=");
if (loc != string::npos) {
result += hold.substr(0, loc) + ",";
continue;
}
loc = hold.find("//");
if (loc != string::npos) {
result += hold.substr(0, loc) + ",";
continue;
}
result += hold + ",";
}
return result;
}
void FileStuff::AddSettingValue(char *id, char *setting, char *value) {
this->FindEntry("[" + string(id) + "]");
// New entry, adjust sect_start to point to the end
if (this->entry_start == this->entry_end) {
// If this is a new file do not insert a newline at the top
if (this->buffer.begin() != this->buffer.end())
this->buffer.push_back(LINEFEED);
this->buffer.push_back("[" + string(id) + "]" + LINEFEED);
this->entry_start = this->buffer.end() - 1;
this->entry_end = this->buffer.end();
}
else {
char* delstr = (char*)malloc(sizeof(char) * strlen(setting) + 2);
strcpy(delstr, setting);
if (value != NULL && strlen(value) != 0)
strcat(delstr, "=");
this->RemoveSetting(id, delstr);
free(delstr);
}
// Insert the new setting/value pair into the file buffer
if (value != NULL && strlen(value) != 0)
this->entry_start = this->buffer.insert(this->entry_start + 1, (string)setting + "=" + value + LINEFEED) - 1;
else
this->entry_start = this->buffer.insert(this->entry_start + 1, (string)setting + LINEFEED) - 1;
}
void FileStuff::RemoveSetting(char *id, char *setting) {
vector<string>::iterator del_start;
string str_set = (string)setting;
this->FindEntry("[" + string(id) + "]");
// Nothing to remove, skip this section
if (this->entry_start == this->buffer.end())
return;
del_start = remove_if(this->entry_start, this->entry_end, [str_set](string str) { return str.compare(0, str_set.length(), str_set) == 0; });
if (del_start != this->entry_end) {
buffer.erase(del_start, this->entry_end);
this->FindEntry("[" + string(id) + "]");
}
}
void FileStuff::RemoveEntry(char *id) {
this->FindEntry("[" + string(id) + "]");
if (this->entry_start != this->buffer.end()) {
// Also include any line feeds following the entry's end
// Is this actually needed??? I cannot recall why this bit of code was written
if (this->entry_end != this->buffer.end() && *(this->entry_end + 1) == LINEFEED)
++this->entry_end;
this->buffer.erase(this->entry_start, this->entry_end);
this->entry_start = this->buffer.end();
this->entry_end = this->buffer.end();
}
}
void FileStuff::WriteToFile() {
ofstream myFile;
// Write the modified file buffer to memory
myFile.open(this->fullpath, ios::binary);
// Write each line to file
for (vector<string>::iterator it = this->buffer.begin(); it != this->buffer.end(); ++it)
myFile << *it;
// Clean-up
myFile.close();
// The buffer and file are the same right now, update the file time
_stat(this->fullpath.c_str(), &this->file_time);
this->entry_start = this->buffer.end();
this->entry_end = this->buffer.end();
}
/////////////////////////////////
// CLASS IMPLEMENTATION (PRIVATE)
/////////////////////////////////
void FileStuff::LoadFile() {
ifstream myFile;
string junk;
// Check if the file is loaded
if (!this->buffer.empty() && !this->HasChanged())
return;
// Clear the cache
this->buffer.clear();
// Open the file for reading in binary mode (The file will be copied byte for byte)
myFile.open(fullpath, ios_base::binary);
myFile.seekg(0, ios::beg);
// Line by line parsing, if this is too slow move to reading chunks at a time into a buffer
while (getline(myFile, junk))
this->buffer.push_back(junk + "\n");
// Before closing the file get the file times
_stat(fullpath.c_str(), &this->file_time);
this->last_checked = time(NULL);
// Clean up by closing the file handle
if (myFile.is_open())
myFile.close();
}
bool FileStuff::HasChanged() {
struct _stat current_filetime;
time_t current_time;
// check_time will decide how often to check if the file has changed
current_time = time(NULL);
if (current_time - this->last_checked > this->check_time)
return false;
// Compare the file time and the last stored time for the file
_stat(this->fullpath.c_str(), &current_filetime);
if (current_filetime.st_mtime != this->file_time.st_mtime)
return true;
return false;
}
void FileStuff::FindEntry(string search) {
// regex to exactly match a game ID [00000000-00000000-C:00]
// Comments are optional at the end but they can be read as well
regex expr ("^\\[([A-Fa-f0-9]){8}-([A-Fa-f0-9]){8}-C:([A-Fa-f0-9]){2}\\]\\s*(?://?.*?\\s*)?$");
// regex to match, at the start [, one or more letters and/or digits and/or spaces, a ],
// optional spaces, a comment, followed by anything and ending in whitespace characters (any amount)
regex expr2 ("^\\[([A-Za-z0-9]|\\s)+\\]\\s*(?://?.*?\\s*)?$");
this->LoadFile();
if (this->last_offset < this->buffer.size() && this->buffer[last_offset].compare(0, search.length(), search) != 0) {
// Reset the start and end to bad values
this->entry_start = this->buffer.end();
this->entry_end = this->buffer.end();
// Use std algorithm to find the first entry
this->entry_start = find_if(this->buffer.begin(), this->buffer.end(), [search](string p) { return p.compare(0, search.length(), search) == 0;});
}
else
this->entry_start = this->buffer.begin() + this->last_offset;
// Find the next entry or the end of the file
if (this->entry_start != this->buffer.end()) {
this->last_offset = distance(this->buffer.begin(), this->entry_start);
this->entry_end = find_if(this->entry_start + 1, this->buffer.end(), [search, expr, expr2](string p) { return (regex_match(p, expr) || regex_match(p, expr2));});
}
else
this->entry_end = this->buffer.end();
}
//////////////////////////
// INTERFACE WITH CLASS
//////////////////////////
FileStuff *GetTheFile(char *filename) {
vector<FileStuff>::iterator it;
FileStuff file;
// Check if the file has previously been loaded and exists in the vector
for (it = myFiles.begin(); it != myFiles.end(); ++it) {
if ((*it).GetFileName() == (string)filename)
return &(*it);
}
// File has not been loaded
file = *new FileStuff(filename);
myFiles.push_back(file);
return &myFiles.back();
}
string ReadHandler(char *filename, char *id, char *setting, char *def, BOOLEAN getting_value) {
string search, value;
DWORD wait_result;
FileStuff *file;
// The search string
search = "[" + (string)id + "]";
// Simple mutex to prevent accessing memory across multiple threads
wait_result = WaitForSingleObject(gLVMutex, INFINITE);
// Some kind of error!?
if (wait_result != WAIT_OBJECT_0)
return NULL;
// Fetch the handle to the file
file = GetTheFile(filename);
// Null setting denotes we're fetching key names
// Used in Rom Status to load the colors ahead of time (Before the name is known)
if (setting != NULL)
value = file->GetValue(id, setting, def, getting_value == TRUE ? true : false);
else
value = file->GetKeys(id);
if (!ReleaseMutex(gLVMutex))
MessageBox(NULL, "Failed to release a mutex???", "Error", MB_OK);
return value;
}
void WriteHandler(char *filename, char *id, char *setting, char *value, supported_writes write_type) {
string built_id;
DWORD wait_result;
FileStuff *file;
built_id = "[" + (string)id + "]";
// Simple mutex to prevent accessing memory across multiple threads
wait_result = WaitForSingleObject(gLVMutex, INFINITE);
// Some kind of error!?
if (wait_result != WAIT_OBJECT_0)
return;
file = GetTheFile(filename);
// To do
// Write a section here that verifies there is at least 1 setting left
// If there would be 0 settings left that would be equivalent to an entry deletion
// Make any modifications to the file buffer before writing to file
switch (write_type) {
case WRITE_ENTRY:
file->AddSettingValue(id, setting, value);
break;
case DELETE_ENTRY:
file->RemoveEntry(id);
break;
case DELETE_SETTING:
file->RemoveSetting(id, setting);
break;
}
// If the entry was not deleted then sort the section
if (write_type != DELETE_ENTRY)
file->SortEntry(built_id);
file->WriteToFile();
if (!ReleaseMutex(gLVMutex))
MessageBox(NULL, "Failed to release a mutex???", "Error", MB_OK);
}
//////////////////////////
// THESE INTERFACE WITH C
//////////////////////////
char *ReadStr(char *filename, char *id, char *setting, char *defaultvalue) {
string result;
char *ret;
result = ReadHandler(filename, id, setting, defaultvalue, TRUE);
// Does this need error-checking? A non-null default should not be passed.
ret = (char *)malloc(sizeof(char) * (result.length() + 1));
if (ret != NULL)
strcpy(ret, result.c_str());
return ret;
}
int IsSet(char *filename, char *id, char *setting) {
string result = ReadHandler(filename, id, setting, STR_FALSE, FALSE);
return (result.compare(STR_FALSE) == 0) ? FALSE : TRUE;
}
int FetchIntValue(char *filename, char *id, char *setting, int def) {
string result = ReadHandler(filename, id, setting, STR_FALSE, TRUE);
return (result.compare(STR_FALSE) == 0) ? def : atoi(result.c_str());
}
void Write(char *filename, char *id, char *setting, char *value) {
WriteHandler(filename, id, setting, value, WRITE_ENTRY);
}
void Delete(char *filename, char *id, char *setting) {
WriteHandler(filename, id, setting, NULL, DELETE_SETTING);
}
void DeleteAll(char *filename, char *id) {
WriteHandler(filename, id, NULL, NULL, DELETE_ENTRY);
}