libconfini Yet another INI parser

Compiling without I/O

What `./configure --without-io-api` does

Almost everything in libconfini is implemented from scratch, with the only notable exception of the I/O functions `load_ini_file()` and `load_ini_path()`, which rely on standard libraries – either the C Standard or the POSIX Standard, depending on the build settings. On some platforms, however, only a rather exotic I/O API is available, while for some other platforms the C Standard Library is simply too heavy or just not implementable.

In the past, the build environment of libconfini did not offer shortcuts for facing this kind of situations – although, thanks to the modularity of the source code, it was still relatively simple to get rid of every tie with the C Standard Library and compile libconfini as “bare metal”, with `strip_ini_cache()` as the only parsing function (as this relies only on a buffer for its input), i.e. without `load_ini_file()` and `load_ini_path()`, and possibly even without including any header at all.

Starting from version 1.13.0 a “bare metal” version of libconfini has been made available by simply passing a `--without-io-api` option to the `configure` script. This modified version has the following characteristics:

- No heap usage (no memory is ever allocated or freed)
- No I/O functions (it is possible to parse only disposable `char` buffers via `strip_ini_cache()`)
- Everything else is inherited verbatim from the official version

# The `dev/hackings/baremetal/` subdirectory

When the `configure` script is launched with the `--without-io-api` option (or, equivalently, with `--with-io-api=baremetal`), it assumes that no standard library at all could be present in the system. Hence it runs a series of tests and creates an inventory of what is present and what is not, in order to amend the source code accordingly – to ignore all the tests and assume that literally nothing from the C Standard Library is supported, use `--without-libc`. The amendments are necessary (instead of just relying on the C preprocessor) because it is required to change the public header, not just the compiled code.

Only a very small amount of code in libconfini depends on the C Standard Library besides the I/O functions, so it is relatively easy to produce a “bare metal” fork with or without the latter. The `dev/hackings/baremetal` subdirectory contains all the necessary amendments. These are automatically applied when launching `make all` or `make baremetal-source-code` after having launched `./configure --without-io-api` (the original source code will be preserved).

The `str2num.c` file contains re-implementation of the functions `ini_get_int()`, `ini_get_lint()`, `ini_get_llint()`, `ini_get_float()` and `ini_get_double()`, which in the original code are implemented as pointers to standard functions (see below). This file amends `src/confini.c`.

The `str2num.h` file, which amends `src/confini.h` (i.e. the public header), exports the function headers of what `str2num.c` implements.

The `confini-header.c` file contains only a nominal workaround-amendment for `src/confini.c` (for facilitating the build system) that does not change the final C code compiled.

To create the source code of a “bare metal” version of libconfini a fifth amendment to the public header is also required, containing some common C standard definitions. This amendment is automatically generated for each platform during the build process and will be located under `no-dist/hackings/baremetal/c-standard-library.h`.

Here follows the summary of what is required by `./configure --without-io-api`:

1. `dev/hackings/baremetal/confini-header.c` (pasted to the private module `src/confini.c`)
2. `dev/hackings/baremetal/str2num.c` (pasted to the private module `src/confini.c`)
3. `dev/hackings/baremetal/str2num.h` (pasted to the public header `src/confini.h`)
4. `no-dist/hackings/baremetal/c-standard-library.h` (pasted to the public header `src/confini.h` after having been automaticaly generated either by the `configure` script, as an exact copy of `dev/hackings/baremetal/c-standard-library.h`, or by `make approve-revision`, in the few cases where manual user's intervention is required during the build process)

The first four files (the ones located in the `dev/hackings/baremetal` subdirectory) are static and do not need any intervention from the user, unless (s)he wants to participate in the development of libconfini. The fifth file might require manual intervention in some situations, depending on the platform or the user's will (the build system will emit a warning in such cases).

# Re-implementing `load_ini_file()` and `load_ini_path()` for your platform

Whether you are using the bare-metal version of libconfini for a regular computer, a fridge or a microwave oven, you might have eventually to deal with some kind of filesystem. If the C standard `fopen()`, `fseek()`, `ftell()`, `rewind()`, `fread()` and `fclose()` do not suit your needs, you can re-implement your own version of `load_ini_file()` and `load_ini_path()`. The only requirement is that at the end of the day you find a way to pass a disposable buffer containing an entire INI file to `strip_ini_cache()`.

A good way to proceed is to hack the original pair of functions that rely on the C standard I/O API and adapt them to your platform:

#include <stdio.h>
#include <stdlib.h>
int load_ini_file (
FILE * const ini_file,
const IniFormat format,
const IniStatsHandler f_init,
const IniDispHandler f_foreach,
void * const user_data
) {
long int file_size;
if (fseek(ini_file, 0, SEEK_END) || (file_size = ftell(ini_file)) < 0) {
return CONFINI_EBADF;
}
if (file_size > SIZE_MAX) {
return CONFINI_EFBIG;
}
char * const cache = (char *) malloc((size_t) file_size + 1);
if (!cache) {
return CONFINI_ENOMEM;
}
rewind(ini_file);
if (fread(cache, 1, (size_t) file_size, ini_file) < file_size) {
free(cache);
return CONFINI_EIO;
}
const int return_value = strip_ini_cache(
cache,
(size_t) file_size,
format,
f_init,
f_foreach,
user_data
);
free(cache);
return return_value;
}
int load_ini_path (
const char * const path,
const IniFormat format,
const IniStatsHandler f_init,
const IniDispHandler f_foreach,
void * const user_data
) {
FILE * const ini_file = fopen(path, "rb");
if (!ini_file) {
return CONFINI_ENOENT;
}
long int file_size;
if (fseek(ini_file, 0, SEEK_END) || (file_size = ftell(ini_file)) < 0) {
return CONFINI_EBADF;
}
if (file_size > SIZE_MAX) {
return CONFINI_EFBIG;
}
char * const cache = (char *) malloc((size_t) file_size + 1);
if (!cache) {
return CONFINI_ENOMEM;
}
rewind(ini_file);
if (fread(cache, 1, (size_t) file_size, ini_file) < file_size) {
free(cache);
return CONFINI_EIO;
}
/* No checks here, as there is nothing we can do about it... */
fclose(ini_file);
const int return_value = strip_ini_cache(
cache,
(size_t) file_size,
format,
f_init,
f_foreach,
user_data
);
free(cache);
return return_value;
}
IniDispHandler
int(* IniDispHandler)(IniDispatch *dispatch, void *user_data)
Callback function for handling an IniDispatch structure.
Definition: confini.h:162
strip_ini_cache
int strip_ini_cache(register char *const ini_source, const size_t ini_length, const IniFormat format, const IniStatsHandler f_init, const IniDispHandler f_foreach, void *const user_data)
Parse and tokenize a buffer containing an INI file, then dispatch its content to a custom callback.
Definition: confini.c:2605
IniStatsHandler
int(* IniStatsHandler)(IniStatistics *statistics, void *user_data)
Callback function for handling an IniStatistics structure.
Definition: confini.h:153
CONFINI_ENOMEM
@ CONFINI_ENOMEM
Definition: confini.h:412
CONFINI_EFBIG
@ CONFINI_EFBIG
Definition: confini.h:418
CONFINI_EIO
@ CONFINI_EIO
Definition: confini.h:413
CONFINI_EBADF
@ CONFINI_EBADF
Definition: confini.h:416
CONFINI_ENOENT
@ CONFINI_ENOENT
Definition: confini.h:411
load_ini_path
int load_ini_path(const char *const path, const IniFormat format, const IniStatsHandler f_init, const IniDispHandler f_foreach, void *const user_data)
Parse an INI file and dispatch its content to a custom callback using a path as argument.
Definition: confini.c:3274
load_ini_file
int load_ini_file(FILE *const ini_file, const IniFormat format, const IniStatsHandler f_init, const IniDispHandler f_foreach, void *const user_data)
Parse an INI file and dispatch its content to a custom callback using a FILE structure as argument.
Definition: confini.c:3180
IniFormat
24-bit bitfield representing the format of an INI file (INI dialect)
Definition: confini.h:115

# Going manual

If instead of using the `--without-io-api` option you prefer to adapt the code manually, follow these simple steps (or adjust them according to your needs), which map verbatim what `--without-io-api` does:

1. Remove `#include <stdio.h>` from `confini.h`
2. If the `stddef.h` header (providing the `typedef` for the `size_t` data type) is available, add `#include <stddef.h>` to `confini.h`, otherwise provide your own implementation of the `size_t` data type
3. If the `stdint.h` header (providing the `typedef` for the `int8_t`, `uint8_t`, `uint16_t` and `uint32_t` data types) is not available, remove it from `confini.h` and provide your own implementation of the `int8_t`, `uint8_t`, `uint16_t` `uint32_t` data types
4. Remove the functions `load_ini_file()` and `load_ini_path()` from both `confini.h` and `confini.c`
5. If the `stdlib.h` header is not available, remove `#include <stdlib.h>` from `confini.c` and remove the function pointers `ini_get_int`, `ini_get_lint`, `ini_get_llint` and `ini_get_double` from both `confini.h` and `confini.c` (you will have to use your own functions for converting strings to numbers) – for a set of possible replacements as actual functions instead of function pointers, please see below)
6. Leave everything else as it is

After doing so libconfini will work even without a kernel.

## Parsing numbers without the C Standard Library

As explained above, compiling libconfini without the C Standard Library requires getting rid of the function pointers `ini_get_int()`, `ini_get_lint()`, `ini_get_llint()` and `ini_get_double()` – as these are nothing but links to the standard functions `atoi()`, `atol()`, `atoll()` and `atof()`.

Instead of just removing these function pointers, it is also possible to provide novel functions implemented from scratch for parsing numbers.

What `./configure --without-io-api` does in fact is replacing in `src/confini.h` the function pointers declared immediately after the comment `/* PUBLIC LINKS */` with the following function headers:

extern int ini_get_int (
const char * const ini_string
);
extern long int ini_get_lint (
const char * const ini_string
);
extern long long int ini_get_llint (
const char * const ini_string
);
extern float ini_get_float (
const char * const ini_string
);
extern double ini_get_double (
const char * const ini_string
);
ini_get_float
double(*const ini_get_float)(const char *ini_string)
Legacy support for parsing a double data type – please do not use this function: use ini_get_double()...
Definition: confini.c:5755
ini_get_llint
long long int(*const ini_get_llint)(const char *ini_string)
Pointer to atoll()
Definition: confini.c:5737
ini_get_double
double(*const ini_get_double)(const char *ini_string)
Pointer to atof()
Definition: confini.c:5739
ini_get_int
int(*const ini_get_int)(const char *ini_string)
Pointer to atoi()
Definition: confini.c:5733
ini_get_lint
long int(*const ini_get_lint)(const char *ini_string)
Pointer to atol()
Definition: confini.c:5735

The same `configure` option amends in `src/confini.c` (at the end of the file) the corresponding pointers with the code below. Note that the C language does not possess a templating mechanism, so the following code needs to rely on a macro for not repeating five times the same function body with only minimal variations.

/*
The code below is free software. You can redistribute it and/or modify it
under the terms of the GPL license version 3 or any later version. See
COPYING for details.
*/
#define _CONFINI_RADIX_POINT_ '.'
#define __INTEGER_DATA_TYPE__ 0
#define __FLOAT_DATA_TYPE__ 1
/*
Mask `abcd` (6 bits used):
FLAG_1 Continue the loop
FLAG_2 The numerical part has been found
FLAG_4 This is the sign
FLAG_8 Radix point has not been encountered yet
FLAG_16 This is a digit
FLAG_32 The number is negative
*/
#define _CONFINI_STR2NUM_FNBODY_(HAS_RADIX_PT, DATA_TYPE, STR) \
register uint_least8_t abcd = 9; \
register size_t idx = 0; \
register DATA_TYPE retval = 0; \
__PP_IIF__(HAS_RADIX_PT, \
DATA_TYPE fact = 1; \
) \
do { \
abcd = \
STR[idx] > 47 && STR[idx] < 58 ? \
abcd | 18 \
: !(abcd & 6) && STR[idx] == '-' ? \
(abcd & 47) | 36 \
: !(abcd & 6) && STR[idx] == '+' ? \
(abcd & 15) | 4 \
__PP_IIF__(HAS_RADIX_PT, \
: (abcd & 8) && STR[idx] == _CONFINI_RADIX_POINT_ ? \
(abcd & 39) | 2 \
) \
: !(abcd & 2) && is_some_space(STR[idx], _CONFINI_WITH_EOL_) ? \
(abcd & 47) | 1 \
: \
abcd & 46; \
if (abcd & 16) { \
retval = __PP_IIF__(HAS_RADIX_PT, \
abcd & 8 ? \
STR[idx] - 48 + retval * 10 \
: \
(STR[idx] - 48) / (fact *= 10) + retval, \
retval * 10 + STR[idx] - 48 \
); \
} \
idx++; \
} while (abcd & 1); \
return abcd & 32 ? -retval : retval;
int ini_get_int (const char * const ini_string) {
_CONFINI_STR2NUM_FNBODY_(__INTEGER_DATA_TYPE__, int, ini_string);
}
long int ini_get_lint (const char * const ini_string) {
_CONFINI_STR2NUM_FNBODY_(__INTEGER_DATA_TYPE__, long int, ini_string);
}
long long int ini_get_llint (const char * const ini_string) {
_CONFINI_STR2NUM_FNBODY_(__INTEGER_DATA_TYPE__, long long int, ini_string);
}
float ini_get_float (const char * const ini_string) {
_CONFINI_STR2NUM_FNBODY_(__FLOAT_DATA_TYPE__, float, ini_string);
}
double ini_get_double (const char * const ini_string) {
_CONFINI_STR2NUM_FNBODY_(__FLOAT_DATA_TYPE__, double, ini_string);
}