Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Wednesday, December 7, 2022

Reading a configuration file into a C++ application

 

Carrying on with my network-switchport-mapping-program, here’s the latest thing I was working on; making a configuration file. Because I keep changing where I am, so also does my active network connection (in other words, at work the IP address is something like 10.x.x.x and at home its 192.x.x.x). The thing is, it isn’t going to just by myself who might end up using this program – so to make this as usable and adaptive as possible, some settings need to manually be added that are relevant to a particular environment.

How would you go about doing this? You could use something like cin or scanf (although, which one is best? (Or this)), but this would mean having to use user input in your program. So if you want to automate a program to not have to use any user input to determine its operation, a simple text file can work very well at providing values to certain variables you use in your code.

So far, in my case, this is going to be the (1)Server name, (2)Network IP, (3) Timeout value for receiving a packet and whether or not to capture (4)LLDP and/or (5)CDP packets.

Implementation

First of all I made a new function to handle this in my code called loadConfig(). I could modify it to take a filename as its parameter, but given that this implementation is very program-specific, it doesn’t really matter too much for now.

int loadConfig()
{
	char * fileName = "config.ini";
	FILE *pFile = fopen(fileName, "r");


        #FILE *pFile;
        #fopen_s(&pFile, fileName, "r");

	char buffer2[TEXTSIZE][TEXTSIZE];
	char * delimiters = " =,\n;";	

        //All the rest of the code goes here

	fclose(pFile);
}

The first thing I do here is to make a new string with the name of the text file to open and pass that as a parameter to fopen (or fopen_s), which returns a FILE object. This is also passed a parameter to “read” the file (another option is to be able to write to a file instead).

The next two declarations below this are a 2d buffer that will hold each line of text (the first dimension is a new line, the next dimension is each character of text) and a string of every character that we will use to represent a new “token” in our string. This will be explained later. After we have done everything, we then close the file and carry on (or exit) with our program.

So, extending the above gap in the code further, we can then start to read from the file and break it up a bit using fgets.

...

size_t linesRead = 0;
	
while (fgets(buffer2[linesRead], TEXTSIZE, pFile) != NULL)
{

...

 

This will place text into our buffer above at a new point in the array, with the length of TEXTSIZE (used also in defining the maximum length of our buffer’s elements), from the file we specified above. If it detects the end of the file, it will end the loop and exit out.

size_t linesRead = 0;

while (fgets(buffer2[linesRead], TEXTSIZE, pFile) != NULL)
{		
	if (buffer2[linesRead][0] == '\n' || buffer2[linesRead][0] == '#')
	{			
		continue;
	}

	char * variable;
        #char * next		;
	variable = strtok(buffer2[linesRead], delimiters);
        #variable = strtok_s(buffer2[linesRead], delimiters, &next);		

	if (!variable)
	{
		continue;
	}

//Put anything else you want to do here

	linesRead++;
}

Once the file is opened, the first thing to do is to check the first character. If it is a new line, then we can just skip this as it is blank space and if it is a hashtag (#) we can also skip it as it is a comment. The continue statement will continue the while loop but it will skip over the current iteration (by contrast, if you wanted to break out of the loop entirely, you would use break instead).

You can then use strtok (or strtok_s) to break apart the current string of text further. When you specify the buffer to operate on, it will actually modify it – but we still have to return the result in order to use it (unless you use strtok_s, which stores the next result in next, rather than modifying the original set of results). We also specify the characters, in a string, to use to separate values out. An equals sign will denote to us the end of a variable name and the start of its value, so we remove them, immediately separating out these two values. Spaces may be added in around the equals sign, so we can add in a space too, to the list of values to look for. Although fgets will stop its iteration at the end of a line, the new line character (\n) is still included, so we remove this too and finally, we get rid of semi-colons and commas (I had an idea to use these but haven’t properly, yet).

Note that the linesRead variable is only incremented on a successful iteration of the while loop. This means that, by not incrementing, that element of the buffer is over-written on the next iteration and so empty and invalid lines don’t fill up the buffer.

Now to add in the magic; copying a value from the text to something usable in the program!

if (strcmp(variable, "server") == 0)
{			
        #variable = strtok_s(NULL, delimiters, &next);
	variable = strtok(NULL, delimiters);			

	settingsServer = (char *)malloc((strlen(variable) + 1) * sizeof(char));
	memcpy(settingsServer, variable, strlen(variable));
	settingsServer[strlen(variable)] = '\0';

	printf("Server is %s\n", variable);
}

First of all, we do a string comparison. “variable” is a character pointer and anything in quotes is also a character pointer, too. If the contents of both pointers are the same, the result is 0.

Then we call strtok again, this time passing it NULL as a parameter for the string to operate on. What this will do, is cause strtok to continue from the last string it was working on. If we pass it the same delimiters, it will, essentially, separate out everything that isn’t a delimiter and give us back the start of the next string (until the next delimiter, which will at the very least be a new line).

After this, the next three lines do a memory allocation and a memory copy to, essentially, do a string copy. I prefer using this way because it doesn’t complain when you just want to copy data in a format that you know will work (such as when you are using typedefs like u_char) and when you need to copy past a “\0” character (something I might need to do with a raw packet capture), but if anyone knows of a compelling reason to use strcpy over memcpy, I would be interested to hear.

Finally, I add a null terminating character on the end. Its worth being careful about this; the terminating character is important for printing text, but adding strings together and manipulating strings with \0 in the middle could present a problem if using strcpy or strncpy, for example. However, elsewhere in the program I use memcpy to add strings together so this isn’t a problem and I can just exclude these characters myself.

Of course, this is useful for text; but if I want to convert a numerical value to an integer, for example, I need to convert a character (ASCII) to an integer (a number). It would be nice to just use something simply like (int)variable, however the problem is that a two-byte character (e.g. ‘1’ and ‘0’, which we would see to mean the number 10) would be considered two separate numbers which remove the exponents of the decimal numbering system. The way to write a function would be to multiply each character’s value by 10, for as many times as there are columns, in a loop; but the simpler way is to use itoa.

So the full code would be something like this:

 

#include <string>
#include <stdlib.h>

#define TEXTSIZE 64


char * settingsServer;
int settingsNetwork;
int settingsTimeout;
int settingsCDP;
int settingsLLDP;


int loadConfig()
{
	char * fileName = "config.ini";
	FILE *pFile = fopen(fileName, "r");
	char buffer2[TEXTSIZE][TEXTSIZE];
	char * delimiters = " =,\n;";
	
	if (!pFile)
	{
		return 1;
	}

	size_t linesRead = 0;
	
	while (fgets(buffer2[linesRead], TEXTSIZE, pFile) != NULL)
	{
		if (buffer2[linesRead][0] == '\n' || buffer2[linesRead][0] == '#')
		{			
			continue;
		}


		char * variable;
		variable = strtok(buffer2[linesRead], delimiters);
		

		if (!variable)
		{
			continue;
		}
		
		
		if (strcmp(variable, "server") == 0)
		{			
			variable = strtok(NULL, delimiters);
			if (!variable)
			{
				printf("Error in configuration file: no value for server!\n");
				{
					system("PAUSE");
					return 2;
				}
			}
			
			settingsServer = (char *)malloc((strlen(variable) + 1) * sizeof(char));
			memcpy(settingsServer, variable, strlen(variable));
			settingsServer[strlen(variable)] = '\0';
			printf("Server is %s\n", variable);
		}
		else if (strcmp(variable, "network") == 0)
		{
			variable = strtok(NULL, delimiters);
			if (!variable)
			{
				printf("Error in configuration file: no value for network!\n");
				{
					system("PAUSE");
					return 2;
				}
			}
			settingsNetwork = atoi(variable);
			printf("Network is %s\n", variable);
		}
		else if (strcmp(variable, "timeout") == 0)
		{
			variable = strtok(NULL, delimiters);
			if (!variable)
			{
				printf("Error in configuration file: no value for timeout!\n");
				{
					system("PAUSE");
					return 2;
				}
			}
			settingsTimeout = atoi(variable);
			printf("Timeout is %s\n", variable);
		}
		else if (strcmp(variable, "protocdp") == 0)
		{
			variable = strtok(NULL, delimiters);
			if (!variable)
			{
				printf("Error in configuration file: no value for protocdp!\n");
				{
					system("PAUSE");
					return 2;
				}
			}
			if (strcmp(variable, "true") == 0)
			{
				settingsCDP = 1;
			}
			else
			{
				settingsCDP = 0;
			}

			printf("CDP is %s\n", variable);
		}
		else if (strcmp(variable, "protolldp") == 0)
		{
			variable = strtok(NULL, delimiters);
			if (!variable)
			{
				printf("Error in configuration file: no value for protolldp!\n");
				{
					return 2;
				}
			}
			if (strcmp(variable, "true") == 0)
			{
				settingsLLDP = 1;
			}
			else
			{
				settingsLLDP = 0;
			}
			printf("LLDP is %s\n", variable);
		}
		else
		{
			continue;
		}	
		linesRead++;
	}

	fclose(pFile);
	return 0;

}

This will populate a mixture of strings and integers with text from a file in the same output directory called config.ini. It can be any file name and extension, really, so long as the location and the filename in the program match. I added a few more error checks and with some testing it all seems to work fine. The data I gave it in the config.ini file is as follows:

 server = http://studentnet.cst.beds#.ac.uk/success.php;
network = 10;

;

kk d 

 
 
 
 
 

timeout=600;
protocdp=true;
protolldp=false;

There are some blank spaces here, new lines, gaps and junk data; just to see how it would work. Well, it came out ok! (Note that the space at the beginning of the buffer, which was in the original file, is not copied over after the first strtok call, hence why it doesn’t break during being compared directly to the term “server”).

Test1

Test2

The values seem to work fine and look tolerant to user error. In the next step, I will probably have created a way to configure the settings through a user interface which will then generate the config.ini file without manual editing.

 

Footnote: At first I had tried the following for string comparison instead of strcmp:

if (variable == "server")

But of course, this would be comparing pointers. Another solution would have been the following, by dereferencing both pointers:

if (*variable == *"server")

 

1 comment: