forked from lda/telodendria
542 lines
13 KiB
C
542 lines
13 KiB
C
/*
|
|
* Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation files
|
|
* (the "Software"), to deal in the Software without restriction,
|
|
* including without limitation the rights to use, copy, modify, merge,
|
|
* publish, distribute, sublicense, and/or sell copies of the Software,
|
|
* and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
#include <Config.h>
|
|
|
|
#include <stdlib.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
|
|
#ifndef CONFIG_BUFFER_BLOCK
|
|
#define CONFIG_BUFFER_BLOCK 32
|
|
#endif
|
|
|
|
struct ConfigDirective
|
|
{
|
|
Array *values;
|
|
HashMap *children;
|
|
};
|
|
|
|
struct ConfigParseResult
|
|
{
|
|
unsigned int ok:1;
|
|
union
|
|
{
|
|
size_t lineNumber;
|
|
HashMap *confMap;
|
|
} data;
|
|
};
|
|
|
|
typedef enum ConfigToken
|
|
{
|
|
TOKEN_UNKNOWN,
|
|
TOKEN_NAME,
|
|
TOKEN_MACRO_ASSIGNMENT,
|
|
TOKEN_VALUE,
|
|
TOKEN_SEMICOLON,
|
|
TOKEN_BLOCK_OPEN,
|
|
TOKEN_BLOCK_CLOSE,
|
|
TOKEN_MACRO,
|
|
TOKEN_EOF
|
|
} ConfigToken;
|
|
|
|
typedef struct ConfigParserState
|
|
{
|
|
FILE *stream;
|
|
unsigned int line;
|
|
|
|
char *token;
|
|
size_t tokenSize;
|
|
size_t tokenLen;
|
|
ConfigToken tokenType;
|
|
|
|
HashMap *macroMap;
|
|
|
|
} ConfigParserState;
|
|
|
|
unsigned int
|
|
ConfigParseResultOk(ConfigParseResult * result)
|
|
{
|
|
return result ? result->ok : 0;
|
|
}
|
|
|
|
size_t
|
|
ConfigParseResultLineNumber(ConfigParseResult * result)
|
|
{
|
|
return result && !result->ok ? result->data.lineNumber : 0;
|
|
}
|
|
|
|
HashMap *
|
|
ConfigParseResultGet(ConfigParseResult * result)
|
|
{
|
|
return result && result->ok ? result->data.confMap : NULL;
|
|
}
|
|
|
|
void
|
|
ConfigParseResultFree(ConfigParseResult * result)
|
|
{
|
|
/*
|
|
* Note that if the parse was valid, the hash map
|
|
* needs to be freed separately.
|
|
*/
|
|
free(result);
|
|
}
|
|
|
|
Array *
|
|
ConfigValuesGet(ConfigDirective * directive)
|
|
{
|
|
return directive ? directive->values : NULL;
|
|
}
|
|
|
|
HashMap *
|
|
ConfigChildrenGet(ConfigDirective * directive)
|
|
{
|
|
return directive ? directive->children : NULL;
|
|
}
|
|
|
|
static void
|
|
ConfigDirectiveFree(ConfigDirective * directive)
|
|
{
|
|
size_t i;
|
|
|
|
if (!directive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < ArraySize(directive->values); i++)
|
|
{
|
|
free(ArrayGet(directive->values, i));
|
|
}
|
|
|
|
ArrayFree(directive->values);
|
|
|
|
ConfigFree(directive->children);
|
|
|
|
free(directive);
|
|
}
|
|
|
|
void
|
|
ConfigFree(HashMap * conf)
|
|
{
|
|
char *key;
|
|
void *value;
|
|
|
|
while (HashMapIterate(conf, &key, &value))
|
|
{
|
|
ConfigDirectiveFree((ConfigDirective *) value);
|
|
free(key);
|
|
}
|
|
|
|
HashMapFree(conf);
|
|
}
|
|
|
|
static ConfigParserState *
|
|
ConfigParserStateCreate(FILE * stream)
|
|
{
|
|
ConfigParserState *state = malloc(sizeof(ConfigParserState));
|
|
|
|
if (!state)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
state->macroMap = HashMapCreate();
|
|
|
|
if (!state->macroMap)
|
|
{
|
|
free(state);
|
|
return NULL;
|
|
}
|
|
|
|
state->stream = stream;
|
|
state->line = 1;
|
|
state->token = NULL;
|
|
state->tokenSize = 0;
|
|
state->tokenLen = 0;
|
|
state->tokenType = TOKEN_UNKNOWN;
|
|
|
|
return state;
|
|
}
|
|
|
|
static void
|
|
ConfigParserStateFree(ConfigParserState * state)
|
|
{
|
|
char *key;
|
|
void *value;
|
|
|
|
if (!state)
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
free(state->token);
|
|
|
|
while (HashMapIterate(state->macroMap, &key, &value))
|
|
{
|
|
free(key);
|
|
free(value);
|
|
}
|
|
|
|
HashMapFree(state->macroMap);
|
|
|
|
free(state);
|
|
}
|
|
|
|
static int
|
|
ConfigIsNameChar(int c)
|
|
{
|
|
return isdigit(c) || isalpha(c) || (c == '-' || c == '_');
|
|
}
|
|
|
|
static char
|
|
ConfigConsumeWhitespace(ConfigParserState * state)
|
|
{
|
|
int c;
|
|
|
|
while (isspace(c = fgetc(state->stream)))
|
|
{
|
|
if (c == '\n')
|
|
{
|
|
state->line++;
|
|
}
|
|
}
|
|
return c;
|
|
}
|
|
|
|
static void
|
|
ConfigConsumeLine(ConfigParserState * state)
|
|
{
|
|
while (fgetc(state->stream) != '\n');
|
|
state->line++;
|
|
}
|
|
|
|
static void
|
|
ConfigTokenSeek(ConfigParserState * state)
|
|
{
|
|
int c;
|
|
|
|
/* If we already hit EOF, don't do anything */
|
|
if (state->tokenType == TOKEN_EOF)
|
|
{
|
|
return;
|
|
}
|
|
while ((c = ConfigConsumeWhitespace(state)) == '#')
|
|
{
|
|
ConfigConsumeLine(state);
|
|
}
|
|
|
|
/*
|
|
* After all whitespace and comments are consumed, identify the
|
|
* token by looking at the next character
|
|
*/
|
|
|
|
if (feof(state->stream))
|
|
{
|
|
state->tokenType = TOKEN_EOF;
|
|
return;
|
|
}
|
|
if (ConfigIsNameChar(c))
|
|
{
|
|
state->tokenLen = 0;
|
|
|
|
/* Read the key/macro into state->token */
|
|
if (!state->token)
|
|
{
|
|
state->tokenSize = CONFIG_BUFFER_BLOCK;
|
|
state->token = malloc(CONFIG_BUFFER_BLOCK);
|
|
}
|
|
state->token[state->tokenLen] = c;
|
|
state->tokenLen++;
|
|
|
|
while (ConfigIsNameChar((c = fgetc(state->stream))))
|
|
{
|
|
state->token[state->tokenLen] = c;
|
|
state->tokenLen++;
|
|
|
|
if (state->tokenLen >= state->tokenSize)
|
|
{
|
|
state->tokenSize += CONFIG_BUFFER_BLOCK;
|
|
state->token = realloc(state->token,
|
|
state->tokenSize);
|
|
}
|
|
}
|
|
|
|
state->token[state->tokenLen] = '\0';
|
|
state->tokenLen++;
|
|
|
|
if (!isspace(c))
|
|
{
|
|
state->tokenType = TOKEN_UNKNOWN;
|
|
}
|
|
else
|
|
{
|
|
state->tokenType = TOKEN_NAME;
|
|
|
|
if (c == '\n')
|
|
{
|
|
state->line++;
|
|
}
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
switch (c)
|
|
{
|
|
case '=':
|
|
state->tokenType = TOKEN_MACRO_ASSIGNMENT;
|
|
break;
|
|
case '"':
|
|
state->tokenLen = 0;
|
|
state->tokenType = TOKEN_VALUE;
|
|
|
|
/* read the value into state->curtok */
|
|
while ((c = fgetc(state->stream)) != '"')
|
|
{
|
|
if (c == '\n')
|
|
{
|
|
state->line++;
|
|
}
|
|
/*
|
|
* End of the stream reached without finding
|
|
* a closing quote
|
|
*/
|
|
if (feof(state->stream))
|
|
{
|
|
state->tokenType = TOKEN_EOF;
|
|
break;
|
|
}
|
|
state->token[state->tokenLen] = c;
|
|
state->tokenLen++;
|
|
|
|
if (state->tokenLen >= state->tokenSize)
|
|
{
|
|
state->tokenSize += CONFIG_BUFFER_BLOCK;
|
|
state->token = realloc(state->token,
|
|
state->tokenSize);
|
|
}
|
|
}
|
|
state->token[state->tokenLen] = '\0';
|
|
state->tokenLen++;
|
|
break;
|
|
case ';':
|
|
state->tokenType = TOKEN_SEMICOLON;
|
|
break;
|
|
case '{':
|
|
state->tokenType = TOKEN_BLOCK_OPEN;
|
|
break;
|
|
case '}':
|
|
state->tokenType = TOKEN_BLOCK_CLOSE;
|
|
break;
|
|
case '$':
|
|
state->tokenLen = 0;
|
|
/* read the macro name into state->curtok */
|
|
while (ConfigIsNameChar(c = fgetc(state->stream)))
|
|
{
|
|
state->token[state->tokenLen] = c;
|
|
state->tokenLen++;
|
|
|
|
if (state->tokenLen >= state->tokenSize)
|
|
{
|
|
state->tokenSize += CONFIG_BUFFER_BLOCK;
|
|
state->token = realloc(state->token,
|
|
state->tokenSize);
|
|
}
|
|
}
|
|
state->token[state->tokenLen] = '\0';
|
|
state->tokenLen++;
|
|
state->tokenType = TOKEN_MACRO;
|
|
|
|
ungetc(c, state->stream);
|
|
break;
|
|
default:
|
|
state->tokenType = TOKEN_UNKNOWN;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Resize curtok to only use the bytes it needs */
|
|
if (state->tokenLen)
|
|
{
|
|
state->tokenSize = state->tokenLen;
|
|
state->token = realloc(state->token, state->tokenSize);
|
|
}
|
|
}
|
|
|
|
static int
|
|
ConfigExpect(ConfigParserState * state, ConfigToken tokenType)
|
|
{
|
|
return state->tokenType == tokenType;
|
|
}
|
|
|
|
|
|
static HashMap *
|
|
ConfigParseBlock(ConfigParserState * state, int level)
|
|
{
|
|
HashMap *block = HashMapCreate();
|
|
|
|
ConfigTokenSeek(state);
|
|
|
|
while (ConfigExpect(state, TOKEN_NAME))
|
|
{
|
|
char *name = malloc(state->tokenLen + 1);
|
|
|
|
strcpy(name, state->token);
|
|
|
|
ConfigTokenSeek(state);
|
|
if (ConfigExpect(state, TOKEN_VALUE) || ConfigExpect(state, TOKEN_MACRO))
|
|
{
|
|
ConfigDirective *directive;
|
|
|
|
directive = malloc(sizeof(ConfigDirective));
|
|
directive->children = NULL;
|
|
directive->values = ArrayCreate();
|
|
|
|
while (ConfigExpect(state, TOKEN_VALUE) ||
|
|
ConfigExpect(state, TOKEN_MACRO))
|
|
{
|
|
|
|
char *dval;
|
|
char *dvalCpy;
|
|
|
|
if (ConfigExpect(state, TOKEN_VALUE))
|
|
{
|
|
dval = state->token;
|
|
}
|
|
else if (ConfigExpect(state, TOKEN_MACRO))
|
|
{
|
|
dval = HashMapGet(state->macroMap, state->token);
|
|
if (!dval)
|
|
{
|
|
goto error;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dval = NULL; /* Should never happen */
|
|
}
|
|
|
|
/* dval is a pointer which is overwritten with the next
|
|
* token. */
|
|
dvalCpy = malloc(strlen(dval) + 1);
|
|
strcpy(dvalCpy, dval);
|
|
|
|
ArrayAdd(directive->values, dvalCpy);
|
|
ConfigTokenSeek(state);
|
|
}
|
|
|
|
if (ConfigExpect(state, TOKEN_BLOCK_OPEN))
|
|
{
|
|
/* token_seek(state); */
|
|
directive->children = ConfigParseBlock(state, level + 1);
|
|
if (!directive->children)
|
|
{
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Append this directive to the current block,
|
|
* overwriting a directive at this level with the same name.
|
|
*
|
|
* Note that if a value already exists with this name, it is
|
|
* returned by HashMapSet() and then immediately passed to
|
|
* ConfigDirectiveFree(). If the value does not exist, then
|
|
* NULL is sent to ConfigDirectiveFree(), making it a no-op.
|
|
*/
|
|
ConfigDirectiveFree(HashMapSet(block, name, directive));
|
|
}
|
|
else if (ConfigExpect(state, TOKEN_MACRO_ASSIGNMENT))
|
|
{
|
|
ConfigTokenSeek(state);
|
|
if (ConfigExpect(state, TOKEN_VALUE))
|
|
{
|
|
char *valueCopy = malloc(strlen(state->token) + 1);
|
|
|
|
strcpy(valueCopy, state->token);
|
|
free(HashMapSet(state->macroMap, name, valueCopy));
|
|
ConfigTokenSeek(state);
|
|
}
|
|
else
|
|
{
|
|
goto error;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
goto error;
|
|
}
|
|
|
|
if (!ConfigExpect(state, TOKEN_SEMICOLON))
|
|
{
|
|
goto error;
|
|
}
|
|
ConfigTokenSeek(state);
|
|
}
|
|
|
|
if (ConfigExpect(state, level ? TOKEN_BLOCK_CLOSE : TOKEN_EOF))
|
|
{
|
|
ConfigTokenSeek(state);
|
|
return block;
|
|
}
|
|
else
|
|
{
|
|
goto error;
|
|
}
|
|
|
|
error:
|
|
/* Only free the very top level, because this will recurse */
|
|
if (!level)
|
|
{
|
|
ConfigFree(block);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
ConfigParseResult *
|
|
ConfigParse(FILE * stream)
|
|
{
|
|
ConfigParseResult *result;
|
|
HashMap *conf;
|
|
ConfigParserState *state;
|
|
|
|
result = malloc(sizeof(ConfigParseResult));
|
|
state = ConfigParserStateCreate(stream);
|
|
conf = ConfigParseBlock(state, 0);
|
|
|
|
if (!conf)
|
|
{
|
|
result->ok = 0;
|
|
result->data.lineNumber = state->line;
|
|
}
|
|
else
|
|
{
|
|
result->ok = 1;
|
|
result->data.confMap = conf;
|
|
}
|
|
|
|
ConfigParserStateFree(state);
|
|
return result;
|
|
}
|