Skip to content

Commit 0a3bc7e

Browse files
committed
Add streaming API for zero-allocation event based parsing
1 parent 295faad commit 0a3bc7e

3 files changed

Lines changed: 372 additions & 31 deletions

File tree

README.md

Lines changed: 161 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,137 @@
77
![GitHub Stars](https://img.shields.io/github/stars/diffstorm/ini_parser?style=social)
88
![Platforms](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows%20%7C%20macOS-lightgrey)
99

10-
A lightweight, single-header, speed and safety focused INI file parsing library written in C with C++ compatibility. Designed for simplicity and portability, this parser provides a low-footprint solution to decode INI format.
10+
A lightweight, single-header INI file parsing library written in C, with C++ compatibility. Focused on speed and safety, it offers a low-footprint, portable solution for decoding INI files. The library provides two APIs to suit different use cases: fast random access and memory-efficient streaming.
1111

1212
## Features
13-
13+
- **Dual Parsing Modes**
14+
- **Context API**: Builds searchable structure (O(1) lookups)
15+
- **Streaming API**: Zero-allocation event-based parsing
16+
- **Memory Safe**: Automatic cleanup with context API
17+
- **Unicode Ready**: Full UTF-8 support
1418
- **Robust Parsing**: Handles sections, key-value pairs, comments, and empty lines
1519
- **Memory Safe**: Automatic cleanup of allocated resources
1620
- **Whitespace Handling**: Trims leading/trailing spaces in sections and values
1721
- **Thread Safe**: Context-based design enables multi-instance usage
1822
- **Cross-Platform**: C99/C++11 compatible with no external dependencies
19-
- **Configurable**: Adjustable line length via `INI_MAX_LINE_LENGTH`
23+
- **Configurable**: Adjust line length (`INI_MAX_LINE_LENGTH`) and case sensitivity
24+
25+
## API Comparison
26+
27+
| Feature | Context API (`ini_initialize`) | Streaming API (`ini_parse_stream`) |
28+
|------------------------|--------------------------------|------------------------------------|
29+
| **Memory Usage** | Builds full structure | Constant memory |
30+
| **Lookup Speed** | O(1) after init | N/A (sequential) |
31+
| **Best For** | Small files, random access | Large files (>1GB), embedded |
32+
| **Error Handling** | Post-parse validation | Immediate feedback |
33+
| **Thread Safety** | Per-context isolation | Handler must be reentrant |
34+
| **Max File Size** | Limited by memory | Unlimited |
35+
| **Needs init?** | Yes (`ini_initialize` | No |
36+
| **Needs cleanup?** | Yes (`ini_cleanup`) | No |
37+
38+
**Why No Initialization/Cleanup Needed for Streaming:**
39+
- Processes lines directly from input buffer
40+
- Doesn't build any persistent data structures
41+
- All temporary buffers are stack-allocated
42+
- User handles state via `userdata` parameter
43+
44+
**Memory Diagram:**
45+
```
46+
Context API: Streaming API:
47+
+---------------+ +-----------------+
48+
| ini_context_t | | Input Buffer |
49+
| - sections | +-----------------+
50+
| - keyValues | | Line Buffer | (stack)
51+
+---------------+ +-----------------+
52+
| Allocations | | Userdata |
53+
+---------------+ +-----------------+
54+
| No persistent |
55+
| state |
56+
+-----------------+
57+
```
58+
59+
## Choosing an API
60+
61+
**Use Context API When:**
62+
- You need random access to values
63+
- Working with small to medium configs
64+
- Frequent lookups needed after parsing
65+
66+
**Use Streaming API When:**
67+
- Processing large files (>100MB)
68+
- Memory-constrained environments
69+
- Simple validation/transformation needed
70+
- Direct loading to application structures
71+
72+
## Streaming Parsing API
73+
74+
### Core Components
75+
76+
```c
77+
typedef enum {
78+
INI_EVENT_SECTION, // New section found
79+
INI_EVENT_KEY_VALUE, // Key-value pair parsed
80+
INI_EVENT_COMMENT, // Comment line detected
81+
INI_EVENT_ERROR // Parse error encountered
82+
} ini_eventtype_t;
83+
84+
typedef bool (*ini_handler)(
85+
ini_eventtype_t type, // Event type
86+
const char* section, // Current section (if applicable)
87+
const char* key, // Key name (for key-value events)
88+
const char* value, // Value or comment content
89+
void* userdata // Custom user context
90+
);
91+
```
92+
93+
### Functions
94+
95+
#### `bool ini_parse_stream(const char* content, size_t length, ini_handler handler, void* userdata)`
96+
Processes INI content line-by-line
97+
- `content`: Input string (not modified)
98+
- `length`: Input length in bytes
99+
- `handler`: Callback for parsed events
100+
- `userdata`: Custom context pointer
101+
- **Returns**: `false` if handler aborted parsing
20102

21-
## Components
103+
### Usage Example
104+
105+
```c
106+
typedef struct {
107+
int sections;
108+
int keys;
109+
} ParserStats;
110+
111+
bool stats_handler(ini_eventtype_t type,
112+
const char* section,
113+
const char* key,
114+
const char* value,
115+
void* userdata) {
116+
ParserStats* stats = (ParserStats*)userdata;
117+
118+
switch(type) {
119+
case INI_EVENT_SECTION:
120+
stats->sections++;
121+
break;
122+
case INI_EVENT_KEY_VALUE:
123+
stats->keys++;
124+
printf("[%s] %s = %s\n", section, key, value);
125+
break;
126+
case INI_EVENT_ERROR:
127+
fprintf(stderr, "Error in: %s\n", value);
128+
return false; // Abort parsing
129+
}
130+
return true;
131+
}
132+
133+
void process_large_ini(const char* content, size_t length) {
134+
ParserStats stats = {0};
135+
ini_parse_stream(content, length, stats_handler, &stats);
136+
printf("Parsed %d sections with %d keys\n", stats.sections, stats.keys);
137+
}
138+
```
139+
140+
## Context API
22141
23142
### Core Structures
24143
- **`ini_context_t`**: Manages parser state and stored sections
@@ -74,9 +193,35 @@ Retrieves value for specified section/key
74193
- `maxLen`: Maximum buffer size
75194
- **Returns**: `true` if value found and copied
76195
77-
### Configuration Macros
196+
## Usage Example
197+
198+
```c
199+
#include "ini_parser.h"
200+
#include <stdio.h>
201+
202+
int main() {
203+
const char *config =
204+
"[network]\n"
205+
"host = 127.0.0.1\n"
206+
"port = 8080\n";
207+
208+
ini_context_t ctx = {0};
209+
if (ini_initialize(&ctx, config, strlen(config))) {
210+
char host[256], port[16];
211+
if (ini_getValue(&ctx, "network", "host", host, sizeof(host)) &&
212+
ini_getValue(&ctx, "network", "port", port, sizeof(port))) {
213+
printf("Connecting to %s:%s\n", host, port);
214+
}
215+
ini_cleanup(&ctx);
216+
}
217+
return 0;
218+
}
219+
```
220+
221+
## Configuration Macros
78222
- `INI_MAX_LINE_LENGTH`: Maximum allowed line length (default: 256)
79223
- `INI_PARSER_IMPLEMENTATION`: Define to enable implementation inclusion
224+
- `INI_ENABLE_CASE_SENSITIVITY`: Enables case sensitivity for sections, keys and values.
80225

81226
## Error Handling
82227
The parser provides implicit error checking through boolean return values. Common failure scenarios:
@@ -100,35 +245,20 @@ cmake --build .
100245
ctest --output-on-failure
101246
```
102247

103-
## Usage Example
248+
## Performance Tips
104249

105-
```c
106-
#include "ini_parser.h"
107-
#include <stdio.h>
250+
1. **Prefer Streaming API** for files >100MB
251+
2. **Reuse Contexts** when possible
252+
3. **Set INI_MAX_LINE_LENGTH** to match your data
253+
4. **Avoid Case Sensitivity** unless required (`INI_ENABLE_CASE_SENSITIVITY`)
254+
5. **Batch Initializations** for multiple small configs
108255

109-
int main() {
110-
const char *config =
111-
"[network]\n"
112-
"host = 127.0.0.1\n"
113-
"port = 8080\n";
114-
115-
ini_context_t ctx = {0};
116-
if (ini_initialize(&ctx, config, strlen(config))) {
117-
char host[256], port[16];
118-
if (ini_getValue(&ctx, "network", "host", host, sizeof(host)) &&
119-
ini_getValue(&ctx, "network", "port", port, sizeof(port))) {
120-
printf("Connecting to %s:%s\n", host, port);
121-
}
122-
ini_cleanup(&ctx);
123-
}
124-
return 0;
125-
}
126-
```
256+
## Quality Assurance
127257

128-
## Performance
129-
- **O(1) Lookups**: Pre-parsed structure enables fast access
130-
- **Low Memory**: Compact representation with single allocation
131-
- **Efficient Parsing**: Linear time complexity O(n)
258+
- **Valgrind Clean**: Zero memory leaks
259+
- **100% Line Coverage**: Comprehensive test suite
260+
- **Fuzz Tested**: AFL-integrated validation
261+
- **SAN Enabled**: Address/UB sanitizer support
132262

133263
## :snowman: Author
134264

ini_parser.h

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,24 @@ typedef struct
6161
ini_section_t *sections;
6262
} ini_context_t;
6363

64+
typedef enum
65+
{
66+
INI_EVENT_SECTION,
67+
INI_EVENT_KEY_VALUE,
68+
INI_EVENT_COMMENT,
69+
INI_EVENT_ERROR
70+
} ini_eventtype_t;
71+
72+
typedef bool (*ini_handler)(ini_eventtype_t type, const char *section, const char *key, const char *value, void *userdata);
73+
6474
bool ini_initialize(ini_context_t *ctx, const char *content, size_t length);
6575
void ini_cleanup(ini_context_t *ctx);
6676
bool ini_hasSection(const ini_context_t *ctx, const char *section);
6777
bool ini_hasKey(const ini_context_t *ctx, const char *section, const char *key);
6878
bool ini_hasValue(const ini_context_t *ctx, const char *section, const char *key);
6979
bool ini_getValue(const ini_context_t *ctx, const char *section, const char *key,
7080
char *value, size_t maxLen);
81+
bool ini_parse_stream(const char *content, size_t length, ini_handler handler, void *userdata);
7182

7283
#ifdef __cplusplus
7384
}
@@ -455,4 +466,90 @@ bool ini_getValue(const ini_context_t *ctx, const char *section, const char *key
455466
return false;
456467
}
457468

469+
bool ini_parse_stream(const char *content, size_t length, ini_handler handler, void *userdata)
470+
{
471+
if(!content || !handler)
472+
{
473+
return false;
474+
}
475+
476+
const char *ptr = content;
477+
const char *end = content + length;
478+
char line[INI_MAX_LINE_LENGTH];
479+
char current_section[INI_MAX_LINE_LENGTH] = "";
480+
481+
while(ptr < end)
482+
{
483+
// Extract line
484+
const char *line_start = ptr;
485+
486+
while(ptr < end && *ptr != '\n' && *ptr != '\r')
487+
{
488+
ptr++;
489+
}
490+
491+
size_t line_len = ptr - line_start;
492+
493+
// Handle line endings
494+
while(ptr < end && (*ptr == '\n' || *ptr == '\r'))
495+
{
496+
ptr++;
497+
}
498+
499+
// Process line
500+
if(line_len > 0)
501+
{
502+
line_len = line_len < INI_MAX_LINE_LENGTH - 1 ? line_len : INI_MAX_LINE_LENGTH - 1;
503+
memcpy(line, line_start, line_len);
504+
line[line_len] = '\0';
505+
char section[INI_MAX_LINE_LENGTH] = "";
506+
char key[INI_MAX_LINE_LENGTH] = "";
507+
char value[INI_MAX_LINE_LENGTH] = "";
508+
ini_linetype_t type = parseLine(line, section, key, value);
509+
510+
switch(type)
511+
{
512+
case INI_LINE_SECTION:
513+
strncpy(current_section, section, INI_MAX_LINE_LENGTH);
514+
515+
if(!handler(INI_EVENT_SECTION, current_section, NULL, NULL, userdata))
516+
{
517+
return false;
518+
}
519+
520+
break;
521+
522+
case INI_LINE_KEY_VALUE:
523+
if(!handler(INI_EVENT_KEY_VALUE, current_section, key, value, userdata))
524+
{
525+
return false;
526+
}
527+
528+
break;
529+
530+
case INI_LINE_COMMENT:
531+
if(!handler(INI_EVENT_COMMENT, NULL, NULL, line, userdata))
532+
{
533+
return false;
534+
}
535+
536+
break;
537+
538+
case INI_LINE_INVALID:
539+
if(!handler(INI_EVENT_ERROR, NULL, NULL, line, userdata))
540+
{
541+
return false;
542+
}
543+
544+
break;
545+
546+
default:
547+
break;
548+
}
549+
}
550+
}
551+
552+
return true;
553+
}
554+
458555
#endif /* INI_PARSER_IMPLEMENTATION */

0 commit comments

Comments
 (0)