From 039012272857c45b34e26e4f6403436f864e0d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 3 Mar 2018 16:30:23 +0100 Subject: [PATCH] code import --- .gitignore | 6 +++ Makefile | 5 ++ README.md | 38 +++++++++++++- nini.c | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++ nini.h | 69 ++++++++++++++++++++++++++ test.c | 22 +++++++++ 6 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 nini.c create mode 100644 nini.h create mode 100644 test.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f369f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.obj +*.so +*.out +*.a +CMakeLists.txt +cmake-build-*/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7830e64 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +a.out: test.c nini.c + cc -Og test.c nini.c + +run: a.out + ./a.out diff --git a/README.md b/README.md index 9bc4cc1..0355bad 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ -# nini -Nano INI parser +# NINI + +This parser aims to be the smallest possible while supporting a sufficient subset of the INI format. +The parser is meant for use in embedded applications. + +When compiled for a bare metal Cortex-M0, **the whole parser fits in 336 bytes** of Flash and 30 (+buffers) bytes of RAM. + +## Features + +- Basic INI format support +- Accepts both Unix and DOS newlines +- Minimal memory footprint +- The input file can be loaded in multiple pieces of any size. +- Buffer sizes (section, key, value) can be configured in the header file. +- Custom data `void *` for maintaining user context, passed to the callback + +## Sypported syntax + +Any whitespace, except inside a value, is discarded. + +- **Sections** - `[section.name]` +- **Key-value pairs** - `key.foo-bar_baz123 = value lorem ipsum 123` + - Value can contain whitespace, leading and trailing whitespace is removed. + - Ends with either `\r` or `\n` +- **Comment** `# comment ...` + - Ends with either `\r` or `\n` + +## Limitations + +- Not re-entrant, uses a static state variable +- No checks for invalid syntax +- Whitespace inside keys and section names is removed +- Quoted strings and escape sequences are not supported, will be collected as plain text +- Value can't be followed by an inline comment + +See the file `test.c` for an example of the most basic usage; see the header file for more details on the API. diff --git a/nini.c b/nini.c new file mode 100644 index 0000000..a5ed9e3 --- /dev/null +++ b/nini.c @@ -0,0 +1,143 @@ +/// +/// nini - Nano INI parser +/// +/// Written by MightyPork, 2018 +/// MIT license +/// + + +#include "nini.h" + +enum nini_state { + NINI_IDLE, + NINI_SECTION, + NINI_KEY, + NINI_VALUE, + NINI_COMMENT, +}; + +static struct { + uint8_t section_i; + char section[INI_KEY_MAX]; + + uint8_t key_i; + char key[INI_KEY_MAX]; + + uint8_t value_i; + char value[INI_VALUE_MAX]; + bool val_last_space; + + IniParserCallback cb; + void *userdata; + enum nini_state state; +} nini; + +void ini_parse_begin(IniParserCallback callback, void *userData) +{ + ini_parse_reset(); + nini.cb = callback; + nini.userdata = userData; +} + +void ini_parse(const char *data, size_t len) +{ + for (; len > 0; len--) { + char c = *data++; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + if (nini.state != NINI_VALUE && nini.state != NINI_COMMENT) + continue; + } + + switch (nini.state) { + case NINI_IDLE: + if (c == '[') { + nini.state = NINI_SECTION; + nini.section_i = 0; + } + else if (c == '#') { + nini.state = NINI_COMMENT; + } + else { + nini.state = NINI_KEY; + nini.key_i = 0; + nini.value_i = 0; + nini.val_last_space = false; + nini.key[nini.key_i++] = c; + } + break; + + case NINI_COMMENT: + if (c == '\n' || c == '\r') { + nini.state = NINI_IDLE; + } + break; + + case NINI_SECTION: + if (c == ']') { + nini.section[nini.section_i] = 0; + nini.state = NINI_COMMENT; // discard to EOL + break; + } + else if (nini.section_i < INI_KEY_MAX - 1) { + nini.section[nini.section_i++] = c; + } + break; + + case NINI_KEY: + if (c == '=') { + nini.key[nini.key_i] = 0; + nini.state = NINI_VALUE; + } + else if (nini.key_i < INI_KEY_MAX - 1) { + nini.key[nini.key_i++] = c; + } + break; + + case NINI_VALUE: + switch (c) { + case ' ': + case '\t': + if (nini.value_i) nini.val_last_space = true; + break; + case '\r': + case '\n': + nini.value[nini.value_i] = 0; + nini.state = NINI_IDLE; + nini.cb(nini.section, nini.key, nini.value, nini.userdata); + break; + default: + if (nini.val_last_space && nini.value_i < INI_VALUE_MAX - 1) { + nini.value[nini.value_i++] = ' '; + } + + if (nini.value_i < INI_VALUE_MAX - 1) { + nini.value[nini.value_i++] = c; + } + nini.val_last_space = false; + } + } + } +} + +void *ini_parse_end(void) +{ + if (nini.state == NINI_VALUE) { + nini.value[nini.value_i] = 0; + nini.state = NINI_IDLE; + nini.cb(nini.section, nini.key, nini.value, nini.userdata); + } + + return nini.userdata; +} + +void ini_parse_file(const char *text, size_t len, IniParserCallback callback, void *userData) +{ + ini_parse_begin(callback, userData); + ini_parse(text, len); + ini_parse_end(); +} + +void ini_parse_reset(void) +{ + nini.state = NINI_IDLE; +} diff --git a/nini.h b/nini.h new file mode 100644 index 0000000..552f7d9 --- /dev/null +++ b/nini.h @@ -0,0 +1,69 @@ +/// +/// nini - Nano INI parser +/// +/// Written by MightyPork, 2018 +/// MIT license +/// + +#ifndef INIPARSE_H +#define INIPARSE_H + +#include +#include +#include +#include + +// buffer sizes +#define INI_KEY_MAX 20 +#define INI_VALUE_MAX 30 + +/** + * INI parser callback, called for each found key-value pair. + * + * @param section - current section, empty string for global keys + * @param key - found key (trimmed of whitespace) + * @param value - value, trimmed of quotes or whitespace + * @param userData - opaque user data pointer, general purpose + */ +typedef void (*IniParserCallback)(const char *section, const char *key, const char *value, void *userData); + +/** + * Begin parsing a stream + * + * @param callback - key callback to assign + * @param userData - optional user data that will be passed to the callback + */ +void ini_parse_begin(IniParserCallback callback, void *userData); + +/** + * End parse stream. + * Flushes what remains in the buffer and removes callback. + * + * @returns userData or NULL if none + */ +void* ini_parse_end(void); + +/** + * Parse a string (needn't be complete line or file) + * + * @param data - string to parse + * @param len - string length (0 = use strlen) + */ +void ini_parse(const char *data, size_t len); + +/** + * Parse a complete file loaded to string + * + * @param text - entire file as string + * @param len - file length (0 = use strlen) + * @param callback - key callback + * @param userData - optional user data for key callback + */ +void ini_parse_file(const char *text, size_t len, IniParserCallback callback, void *userData); + +/** + * Explicitly reset the parser + */ +void ini_parse_reset(void); + +#endif // INIPARSE_H diff --git a/test.c b/test.c new file mode 100644 index 0000000..8bb00f1 --- /dev/null +++ b/test.c @@ -0,0 +1,22 @@ +#include "nini.h" +#include + +const char *testfile = +"# Comment ...........\n" +"rootkey = Value\r\n" +"\r\n" +" [ Section@name!@+12346 ]\r\n" +"sec.key-a_b-1 = 444\r\n" +"sec.key2 = Test 123456 with spaces\r\n" + "[ this is a section with spaces nthat will be removed ]\n" +"this_one_has_no_eol = 123456"; + + +void test_cb(const char *section, const char *key, const char *value, void *userData) +{ + printf("[%s] >%s< = >%s<\r\n", section, key, value); +} + +int main (void) { + ini_parse_file(testfile, strlen(testfile), test_cb, NULL); +}