Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mofosyne committed Feb 2, 2025
1 parent fc5fc51 commit 6cf2056
Show file tree
Hide file tree
Showing 12 changed files with 1,090 additions and 2 deletions.
16 changes: 16 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
TabWidth: 4
IndentWidth: 4

Language: Cpp

BreakBeforeBraces: Allman
InsertNewlineAtEOF: True
InsertBraces: True
IndentCaseLabels: True

# https://stackoverflow.com/questions/47683910/can-you-set-clang-formats-line-length
ColumnLimit: 200

IndentExternBlock: NoIndent

BinPackArguments: false
24 changes: 24 additions & 0 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: C/C++ CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up C environment
run: |
sudo apt-get update
sudo apt-get install -y build-essential
- name: Run make
run: |
make test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ modules.order
Module.symvers
Mkfile.old
dkms.conf

.vscode/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Brian
Copyright (c) 2025 Brian Khuu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
57 changes: 57 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
CC ?= cc
CP ?= cp -f
RM ?= rm -f
MKDIR ?= mkdir -p
INSTALL ?= install

PREFIX ?= /usr/local

CFLAGS += -Wall -std=c99 -pedantic -g2 -Og

.PHONY: all
all: test

.PHONY: readme_update
readme_update:
# Library Version (From clib package metadata)
jq -r '.version' clib.json | xargs -I{} sed -i 's|<version>.*</version>|<version>{}</version>|' README.md
jq -r '.version' clib.json | xargs -I{} sed -i 's|<versionBadge>.*</versionBadge>|<versionBadge>![Version {}](https://img.shields.io/badge/version-{}-blue.svg)</versionBadge>|' README.md

.PHONY: test
test: test.c kv_parse.c kv_parse_buffer.c
@echo "# No Extra Features Enabled"
@$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@
@./test
@$(RM) test

@echo ""
@echo "# KV_PARSE_QUOTED_STRINGS enabled"
@$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ -DKV_PARSE_QUOTED_STRINGS
@./test
@$(RM) test

@echo ""
@echo "# KV_PARSE_WHITESPACE_SKIP enabled"
@$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ -DKV_PARSE_WHITESPACE_SKIP
@./test
@$(RM) test

@echo ""
@echo "# ALL Features Enabled"
@$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ -DKV_PARSE_WHITESPACE_SKIP -DKV_PARSE_QUOTED_STRINGS
@./test
@$(RM) test

@echo ""
@echo "PASSED"

.PHONY: format
format:
# pip install clang-format
clang-format -i *.c
clang-format -i *.h

.PHONY: clean
clean:
$(RM) *.o *.so *.aarch64.elf
$(RM) test
94 changes: 93 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,94 @@
# kv_parse
Composible Key Value Parser

<versionBadge>![Version 1.0.0](https://img.shields.io/badge/version-1.0.0-blue.svg)</versionBadge>
[![C](https://img.shields.io/badge/Language-C-blue.svg)](https://en.wikipedia.org/wiki/C_(programming_language))
[![CI/CD Status Badge](https://github.com/mofosyne/kv_get_value.c/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/mofosyne/kv_get_value/actions)

Composable Key Value Parser

Compared to my other simple kv implementation <https://github.com/mofosyne/kv_get_value.c>
This implementation is a bit more flexible and efficient at handling many key value pairs
as well as various key value formats (ini, etc...) at a cost of being made out of multiple
functions rather than one functions.

However this make it more flexible for you to adapt it for more kinds of key value based inputs.
For example handling ini files, where you can add an additional check in between to parse out
the ini sections `[sections]` before handling the key value pairs.

As before, this implementation will not try to parse out the value output to minimise complexity...
but this will at least handle the most annoying bits about handling key value pairs such as callbacks
and dynamic allocation.

There is plenty of other C ini parser out there like <https://toml.io/en/> if you just need a
configuration language. For more complex INI style parsing (ergo section support) do consider:

* <https://github.com/clibs/inih> : Used by clib themselves, relatively simple. Uses callbacks.
* <https://github.com/madmurphy/libconfini> : More complex and can parse typed data in a multi line manner. Uses Callbacks.

The key design properties that my implementation has:
* Pros:
- No Malloc
- No Callbacks
- Simple and predictable API using FILE pointers and static buffers.
- Thread safe. No global variables required.
- Lightweight and composible. This keeps it small and easy to modify.
* Cons:
- Limited scalability. At least the value buffer can be adjusted for each key call. But intentionally still using a fixed buffer.
- We don't use callbacks with mallocs etc... this does lead to less efficiency. But intentionally chose to keep this project simple.
- During key search, the function repeatedly use fseek() which slows down parsing. This can be sped up with hash maps.
- No error handling. To keep this simple, you will need to add your own error logging such as returning an error code.
But comments should make it easy to identify what to add.
- No multiline handling. To control complexity level of the parser... multiline support is intentionally dropped.
(But due to the composible design, it will be easier for developers to replace the value reader with their own implementation.)

## Buffered API

```c
char *kv_parse_buffer_next_line(char *str, size_t line_count);
char *kv_parse_buffer_check_key(char *str, const char *key);
size_t kv_parse_buffer_get_value(char *str, char *value, size_t value_max);
```
Examples:
```c
int kv_buffer_parse(char *input, const char *key, char *value, unsigned int value_max)
{
for (unsigned int line = 0; (input = kv_parse_buffer_next_line(input, line)) != NULL; line++)
{
char *input_value = NULL;
if ((input_value = kv_parse_buffer_check_key(input, key)) != NULL)
{
int ret = kv_parse_buffer_get_value(input_value, value, value_max);
return ret;
}
}
return 0;
}
```

## FILE API

```c
bool kv_parse_next_line(FILE *file, size_t line_count);
bool kv_parse_check_key(FILE *file, const char *key);
size_t kv_parse_get_value(FILE *file, char *value, size_t value_max);
```
Examples:
```c
int kv_file_parse(FILE *file, const char *key, char *value, unsigned int value_max)
{
rewind(file);
for (unsigned int line = 0; kv_parse_next_line(file, line); line++)
{
if (kv_parse_check_key(file, key))
{
int ret = kv_parse_get_value(file, value, value_max);
return ret;
}
}
return 0;
}
```
13 changes: 13 additions & 0 deletions clib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "kv_parse",
"version": "1.0.0",
"repo": "mofosyne/kv_parse.c",
"description": "Composible Key Value Parser C library for parsing key value strings with zero-copy and no mallocs.",
"license": "MIT",
"src": [
"kv_parse.c",
"kv_parse.h",
"kv_parse_buffer.c",
"kv_parse_buffer.h"
]
}
155 changes: 155 additions & 0 deletions kv_parse.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* @file kv_parse.c
* @brief Composible ANSI C Key-Value Parser
*
* This file contains a parser function that extracts values associated with keys in a formatted
* key-value file (e.g., "key=value" or "key: value").
*
* Copyright (c) 2025 Brian Khuu
* MIT licensed
*/
#include <stdbool.h>
#include <stdio.h>

bool kv_parse_next_line(FILE *file, size_t line_count)
{
char ch = '\0';

/* Check if first line */
if (line_count == 0)
{
/* Already at next line */
return true;
}

/* Advance to next line */
ch = getc(file);
while (ch != EOF)
{
if (ch == '\n')
{
ch = getc(file);
if (ch == EOF)
{
return false;
}

ungetc(ch, file);
return true;
}
ch = getc(file);
}

/* File Finished Reading */
return false;
}

bool kv_parse_check_key(FILE *file, const char *key)
{
long start_of_line = ftell(file);
char ch = getc(file);

#ifdef KV_PARSE_WHITESPACE_SKIP
while (ch == ' ' || ch == '\t')
{
ch = getc(file);
}
#endif

/* Check For Key */
for (int i = 0; ch != EOF && key[i] != '\0'; i++, ch = getc(file))
{
if (ch != key[i])
{
/* End of string. Key was not found */
fseek(file, start_of_line, SEEK_SET);
return false;
}
}

#ifdef KV_PARSE_WHITESPACE_SKIP
while (ch == ' ' || ch == '\t')
{
ch = getc(file);
}
#endif

/* Check For Key Value Delimiter */
if (ch != '=' && ch != ':')
{
fseek(file, start_of_line, SEEK_SET);
return false;
}

/* Key Found. Next position is likely the value */
return true;
}

size_t kv_parse_get_value(FILE *file, char *value, size_t value_max)
{
long start_of_value = ftell(file);
char ch = getc(file);
#ifdef KV_PARSE_WHITESPACE_SKIP
while (ch == ' ' || ch == '\t')
{
ch = getc(file);
}
#endif

/* Copy Value To Buffer */
#ifdef KV_PARSE_QUOTED_STRINGS
int quote = EOF;
int prev = EOF;
#endif
for (int i = 0; i < (value_max - 1); ch = getc(file))
{
if (ch == EOF || ch == '\r' || ch == '\n')
{
/* End Of Line */
fseek(file, start_of_value, SEEK_SET);
if (ch == '\n')
{
ungetc('\n', file);
}
value[i] = '\0';
#ifdef KV_PARSE_WHITESPACE_SKIP
while (i > 0 && (value[i - 1] == ' ' || value[i - 1] == '\t'))
{
i--;
value[i] = '\0';
}
#endif
return i;
}
#ifdef KV_PARSE_QUOTED_STRINGS
else if (quote == EOF && (ch == '\'' || ch == '"'))
{
/* Start Of Quoted String */
quote = ch;
continue;
}
else if (quote != EOF && prev != '\\' && ch == quote)
{
/* End Of Quoted String. Return Value */
fseek(file, start_of_value, SEEK_SET);
value[i] = '\0';
return i;
}
else if (quote != EOF && prev == '\\' && ch == quote)
{
/* Escaped Character In Quoted String */
value[i - 1] = ch == EOF ? '\0' : ch;
continue;
}

prev = ch;
#endif

value[i++] = ch == EOF ? '\0' : ch;
}

/* Value too large for buffer. Don't return a value. */
fseek(file, start_of_value, SEEK_SET);
value[0] = '\0';
return 0;
}
Loading

0 comments on commit 6cf2056

Please sign in to comment.