diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba09336 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gclogger \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1139cac --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +EXE = gclogger + +SRC_DIR = src +OBJ_DIR = obj + +SRC = $(wildcard $(SRC_DIR)/*.c) +OBJ = $(SRC:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o) + +CPPFLAGS += -Iinclude +CFLAGS += -Wall -Wextra -Os + +#LDFLAGS += -Llib +LDLIBS += -lcurl + +.PHONY: all clean + +all: $(EXE) + +# Linking: +$(EXE): $(OBJ) + $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ + +# Compiling: +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c + $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + +# Cleaning: +clean: + $(RM) $(OBJ) + $(RM) $(EXE) diff --git a/gclogger.example.ini b/gclogger.example.ini new file mode 100644 index 0000000..189d8a9 --- /dev/null +++ b/gclogger.example.ini @@ -0,0 +1,45 @@ +; +; Geiger Counter Logger - configuration file +; + +; Serial device filename +; ------------------------ +[device] +port=/dev/ttyUSB0 +baud=115200 + +; GPS location of your sensor +; ----------------------------- +location=Innsbruck, AT +latitude=47.2530 +longitude=11.3969 + +; Polling interval in seconds (60..3600) +; ---------------------------------------- +interval=60 + +; +; Log portal configurations +; +; Uncomment sections to enable them +; + +; Custom REST logging portal +; ---------------------------------------------- +[custlog] +;id=example_device_id +;url=https://api.mygclog.com/log +;param_id=id +;param_cpm=cpm +;param_temp=temp +;param_lng=lng +;param_lat=lat +;param_loc=loc +;param_version=version +;param_time=captured_at + +; Log to CSV file +; ---------------------------------------------- +[csv] +;path=/tmp/gclog.csv + diff --git a/include/gclogger.h b/include/gclogger.h new file mode 100644 index 0000000..ebb8cfa --- /dev/null +++ b/include/gclogger.h @@ -0,0 +1,54 @@ +#ifndef _GCLOGGER_H_ +#define _GCLOGGER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ini.h" +#include "gmc.h" + +#define GCLOGGER_VERSION "0.1" + +typedef struct +{ + bool debug; + char* dev_port; + int dev_baud; + char* dev_location; + float dev_latitude; + float dev_longitude; + int dev_interval; + + char* radmon_user; + char* radmon_pass; + char* safecast_key; + char* safecast_device; + char* netc_id; + char* custlog_url; + char* custlog_id; + char* custlog_param_id; + char* custlog_param_cpm; + char* custlog_param_temp; + char* custlog_param_lng; + char* custlog_param_lat; + char* custlog_param_loc; + char* custlog_param_version; + char* custlog_param_time; + char* csv_path; +} configuration; + +static int confighandler(void* config, const char* section, const char* name, const char* value); +bool str_isset(char *str); +void init_configuration(configuration* config); +void signal_handler(int sig); +bool send_custlog(const configuration config, int cpm, float temperature, const char *version, struct tm *tm); +bool send_tocsv(const configuration config, int cpm, float temperature, const char *version, const struct tm *tm); +void show_usage(); + +#endif \ No newline at end of file diff --git a/include/gmc.h b/include/gmc.h new file mode 100644 index 0000000..24dc175 --- /dev/null +++ b/include/gmc.h @@ -0,0 +1,26 @@ +#ifndef _GC_GMC_H_ +#define _GC_GMC_H_ + +#include +#include +#include +#include +#include +#include + +// GQ Geiger Counter Communication Protocol: http://www.gqelectronicsllc.com/download/GQ-RFC1201.txt + +int gmc_open(const char *device, int baud); +void gmc_close(int device); + +int gmc_get_cpm(int device); +float gmc_get_temperature(int device); +int gmc_get_version(int device, char *buf); + +bool gmc_set_heartbeat_off(int device); + +int gmc_write(int device, const char *cmd); +int gmc_read(int device, char *buf, int length); +bool gmc_flush(int device); + +#endif diff --git a/include/ini.h b/include/ini.h new file mode 100644 index 0000000..7b1f03b --- /dev/null +++ b/include/ini.h @@ -0,0 +1,130 @@ +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#ifndef __INI_H__ +#define __INI_H__ + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Nonzero if ini_handler callback should accept lineno parameter. */ +#ifndef INI_HANDLER_LINENO +#define INI_HANDLER_LINENO 0 +#endif + +/* Typedef for prototype of handler function. */ +#if INI_HANDLER_LINENO +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value, + int lineno); +#else +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value); +#endif + +/* Typedef for prototype of fgets-style reader function. */ +typedef char* (*ini_reader)(char* str, int num, void* stream); + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +int ini_parse(const char* filename, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O (see also + ini_parse_string). */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user); + +/* Same as ini_parse(), but takes a zero-terminated string with the INI data +instead of a file. Useful for parsing INI data from a network socket or +already in memory. */ +int ini_parse_string(const char* string, ini_handler handler, void* user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See http://code.google.com/p/inih/issues/detail?id=21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Chars that begin a start-of-line comment. Per Python configparser, allow + both ; and # comments at the start of a line by default. */ +#ifndef INI_START_COMMENT_PREFIXES +#define INI_START_COMMENT_PREFIXES ";#" +#endif + +/* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + +/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Maximum line length for any line in INI file (stack or heap). Note that + this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +/* Nonzero to allow heap line buffer to grow via realloc(), zero for a + fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is + zero. */ +#ifndef INI_ALLOW_REALLOC +#define INI_ALLOW_REALLOC 0 +#endif + +/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK + is zero. */ +#ifndef INI_INITIAL_ALLOC +#define INI_INITIAL_ALLOC 200 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __INI_H__ */ \ No newline at end of file diff --git a/obj/.gitignore b/obj/.gitignore new file mode 100644 index 0000000..0aaa831 --- /dev/null +++ b/obj/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/src/gclogger.c b/src/gclogger.c new file mode 100644 index 0000000..cb4ab59 --- /dev/null +++ b/src/gclogger.c @@ -0,0 +1,300 @@ +#include "gclogger.h" + +extern char *optarg; +extern int optind; + +static volatile bool running = true; + +static int confighandler(void* config, const char* section, const char* name, const char* value) { + configuration* pconfig = (configuration*)config; + + #define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0 + if (MATCH("device", "port")) { + pconfig->dev_port = strdup(value); + } else if (MATCH("device", "baud")) { + pconfig->dev_baud = atoi(value); + } else if (MATCH("device", "location")) { + pconfig->dev_location = strdup(value); + } else if (MATCH("device", "latitude")) { + pconfig->dev_latitude = atof(value); + } else if (MATCH("device", "longitude")) { + pconfig->dev_longitude = atof(value); + } else if (MATCH("device", "interval")) { + pconfig->dev_interval = atoi(value); + } else if (MATCH("custlog", "url")) { + pconfig->custlog_url = strdup(value); + } else if (MATCH("custlog", "id")) { + pconfig->custlog_id = strdup(value); + } else if (MATCH("custlog", "param_id")) { + pconfig->custlog_param_id = strdup(value); + } else if (MATCH("custlog", "param_cpm")) { + pconfig->custlog_param_cpm = strdup(value); + } else if (MATCH("custlog", "param_temp")) { + pconfig->custlog_param_temp = strdup(value); + } else if (MATCH("custlog", "param_lng")) { + pconfig->custlog_param_lng = strdup(value); + } else if (MATCH("custlog", "param_lat")) { + pconfig->custlog_param_lat = strdup(value); + } else if (MATCH("custlog", "param_loc")) { + pconfig->custlog_param_loc = strdup(value); + } else if (MATCH("custlog", "param_version")) { + pconfig->custlog_param_version = strdup(value); + } else if (MATCH("custlog", "param_time")) { + pconfig->custlog_param_time = strdup(value); + } else if (MATCH("csv", "path")) { + pconfig->csv_path = strdup(value); + } else { + return 0; /* unknown section/name, error */ + } + return 1; +} + +bool str_isset(char *str) { + return str != NULL && str[0] != '\0'; +} + +void init_configuration(configuration* config) { + config->debug = false; + + config->dev_port = ""; + config->dev_baud = 115200; + config->dev_location = ""; + config->dev_latitude = 0.0; + config->dev_longitude = 0.0; + config->dev_interval = 60; + config->custlog_url = ""; + config->custlog_id = ""; + config->custlog_param_id = "id"; + config->custlog_param_cpm = "cpm"; + config->custlog_param_temp = "temp"; + config->custlog_param_lng = "lng"; + config->custlog_param_lat = "lat"; + config->custlog_param_loc = "loc"; + config->custlog_param_version = "version"; + config->custlog_param_time = "time"; + config->csv_path = ""; +} + +void signal_handler(int sig) { + switch (sig) { + case SIGTERM: + case SIGINT: + case SIGQUIT: + case SIGHUP: + running = false; + default: + break; + } +} + +bool send_custlog(const configuration config, int cpm, float temperature, const char *version, struct tm *tm) { + CURL *curl; + CURLcode res; + + curl_global_init(CURL_GLOBAL_DEFAULT); + + curl = curl_easy_init(); + + if(curl) { + char *url_buffer; + size_t url_size; + // first get size of final url + url_size = snprintf(NULL, 0, "%s?%s=%s&%s=%d&%s=%f&%s=%s&%s=%ld&%s=%f&%s=%f&%s=%s", + config.custlog_url, + config.custlog_param_id, config.custlog_id, + config.custlog_param_cpm, cpm, + config.custlog_param_temp, temperature, + config.custlog_param_version, version, + config.custlog_param_time, mktime(tm), + config.custlog_param_lng, config.dev_longitude, + config.custlog_param_lat, config.dev_latitude, + config.custlog_param_loc, config.dev_location); + + // now allocate buffer and build url + url_buffer = (char *)malloc(url_size + 1); + snprintf(url_buffer, url_size+1,"%s?%s=%s&%s=%d&%s=%f&%s=%s&%s=%ld&%s=%f&%s=%f&%s=%s", + config.custlog_url, + config.custlog_param_id, config.custlog_id, + config.custlog_param_cpm, cpm, + config.custlog_param_temp, temperature, + config.custlog_param_version, version, + config.custlog_param_time, mktime(tm), + config.custlog_param_lng, config.dev_longitude, + config.custlog_param_lat, config.dev_latitude, + config.custlog_param_loc, config.dev_location); + + printf("final url: %s\n", url_buffer); + + curl_easy_setopt(curl, CURLOPT_URL, url_buffer); + + #ifdef SKIP_PEER_VERIFICATION + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + #endif + + #ifdef SKIP_HOSTNAME_VERIFICATION + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + #endif + + res = curl_easy_perform(curl); + if(res != CURLE_OK) + printf("curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); + else + printf("curl_easy_perform() ok!"); + + free(url_buffer); + curl_easy_cleanup(curl); + + return true; + } + + return false; +} + +bool send_tocsv(const configuration config, int cpm, float temperature, const char *version, const struct tm *tm) { + FILE *fp; + + if ((fp = fopen(config.csv_path, "a")) == NULL) { + printf("Error while opening the csv file '%s'.\n", config.csv_path); + return false; + } + + char *ts = asctime(tm); + ts[strlen(ts) - 1] = 0; // get rid of \n + fprintf(fp, "%s,%d,%07.3f,%s\n", ts, cpm, temperature, version); + + fclose(fp); + + return true; +} + +void show_usage() { + printf("Geiger Counter Logger\n"); + printf("Version %s\n", GCLOGGER_VERSION); + printf("Copyright (C) 2018 Christoph Haas, christoph.h@sprinternet.at\n\n"); + printf("This program comes with ABSOLUTELY NO WARRANTY.\n"); + printf("This is free software, and you are welcome to redistribute it.\n\n"); + printf("Usage: gclogger -c [-d]\n"); + printf(" -c configuration file path\n"); + printf(" -d enable verbose mode\n\n"); +} + +int main(int argc, char *argv[]) { + int opt = 0; + int gc_fd = -1; + configuration config; + + // setup signal handlers + signal(SIGTERM, signal_handler); + signal(SIGINT, signal_handler); + signal(SIGQUIT, signal_handler); + signal(SIGHUP, signal_handler); + + // parse config file + init_configuration(&config); + while ((opt = getopt(argc, argv, "c:d")) != -1) { + switch (opt) { + case 'd': + config.debug = true; + break; + + case 'c': + if (ini_parse(optarg, confighandler, &config) < 0) { + printf("Can't load config file '%s'\n", optarg); + return 1; + } + printf("Config loaded from '%s': device_port=%s, interval=%d\n", + optarg, config.dev_port, config.dev_interval); + break; + + default: /* '?' */ + show_usage(); + exit(EXIT_FAILURE); + } + } + + // additional check if extra/no arguments where given + if (optind < argc || argc == 1) { + show_usage(); + exit(EXIT_FAILURE); + } + + // check if device port is set + if(!str_isset(config.dev_port)) { + printf("Device port must be set!'\n"); + exit(EXIT_FAILURE); + } + + gc_fd = gmc_open(config.dev_port, config.dev_baud); + + // check if device connection was successful + if(gc_fd == -1) { + printf("Connection to device (%s) failed!'\n", config.dev_port); + exit(EXIT_FAILURE); + } + + // read version + char version[15] = { 0 }; + if(gmc_get_version(gc_fd, version) == -1) { + printf("Unable to read Geiger counter version!'\n"); + exit(EXIT_FAILURE); + } + + if (config.debug) { + printf("GC VERSION: %s\n", version); + } + + time_t last = time(NULL); + int cpm, sum = 0, count = 0, tcount = 0; + float temperature, tsum = 0; + + // main loop + while (running) { + // read cpm + if ((cpm = gmc_get_cpm(gc_fd)) > 0) { + sum += cpm; + count++; + } + + // read temperature + tsum += gmc_get_temperature(gc_fd); + tcount++; + + if (difftime(time(NULL), last) >= config.dev_interval) { + if (count > 0) { + struct tm *tm = gmtime(&last); + cpm = sum / count; + temperature = tsum / tcount; + + if (config.debug) { + printf("CPM: %d (= %d/%d), TEMP: %07.3f, Timestamp: %s\n", cpm, sum, count, temperature, asctime(tm)); + } + + // log to custom REST api + if (str_isset(config.custlog_url)) { + printf("Uploading to %s.\n", config.custlog_url); + if(!send_custlog(config, cpm, temperature, version, tm)) { + printf("Upload to %s failed.\n", config.custlog_url); + } + } + + // log to csv + if (str_isset(config.csv_path)) { + if(!send_tocsv(config, cpm, temperature, version, tm)) { + printf("Logging to %s failed.\n", config.csv_path); + } + } + + time(&last); + sum = tsum = count = tcount = 0; + } else { + printf("Reading ZERO value from Geiger tube.\n"); + } + } + + sleep(1); // sleep one second + } + + gmc_close(gc_fd); + + return EXIT_SUCCESS; +} diff --git a/src/gmc.c b/src/gmc.c new file mode 100644 index 0000000..c3b0a43 --- /dev/null +++ b/src/gmc.c @@ -0,0 +1,102 @@ +#include "gmc.h" + +int gmc_open(const char *device, int baud) { + int gc_fd = -1; + struct termios tio; + + memset(&tio, 0, sizeof(struct termios)); + tio.c_cflag = CS8 | CREAD | CLOCAL; // 8n1 + tio.c_cc[VMIN] = 0; + tio.c_cc[VTIME] = 5; + + int tio_baud = B115200; + switch(baud) { + case 9600: tio_baud = B9600; break; + case 19200: tio_baud = B19200; break; + case 38400: tio_baud = B38400; break; + case 57600: tio_baud = B57600; break; + case 115200: tio_baud = B115200; break; + } + + if ((gc_fd = open(device, O_RDWR)) != -1) { + if (cfsetspeed(&tio, tio_baud) == 0) { // set baud speed + if (tcsetattr(gc_fd, TCSANOW, &tio) == 0) { // apply baud speed change + if (gmc_set_heartbeat_off(gc_fd)) { // disable heartbeat, we use polling + return gc_fd; + } + } + } else { + // something failed + close(gc_fd); + gc_fd = -1; + } + } + + return gc_fd; +} + +void gmc_close(int device) { + close(device); +} + +int gmc_get_cpm(int device) { + const char cmd[] = ">"; + char buf[2] = { 0 }; + + if (gmc_write(device, cmd) == (ssize_t) strlen(cmd)) + gmc_read(device, buf, 2); + + return buf[0] * 256 + buf[1]; +} + +float gmc_get_temperature(int device) { + const char cmd[] = ">"; + char buf[4] = { 0 }; + + if (gmc_write(device, cmd) == (ssize_t) strlen(cmd)) + gmc_read(device, buf, 4); + + int sign = buf[2] == 0 ? 1 : -1; + float temp = buf[0]; + temp += (float) buf[1] / 10; + temp = temp * sign; + return temp; +} + +int gmc_get_version(int device, char *version) { + const char cmd[] = ">"; + + if (gmc_write(device, cmd) == (ssize_t) strlen(cmd)) + return gmc_read(device, version, 14); + + return -1; +} + +bool gmc_set_heartbeat_off(int device) { + const char cmd[] = ">"; + + if (gmc_write(device, cmd) == (ssize_t) strlen(cmd)) + return gmc_flush(device); + + return false; +} + +int gmc_write(int device, const char *cmd) { + return write(device, cmd, strlen(cmd)); +} + +int gmc_read(int device, char *buf, int length) { + return read(device, buf, length); +} + +bool gmc_flush(int device) { + char ch; + + // flush input stream (max 100 bytes) + for (int i = 0; i < 100; i++) { + if (read(device, &ch, 1) == 0) + return true; + } + + return false; +} diff --git a/src/ini.c b/src/ini.c new file mode 100644 index 0000000..7a0a2a0 --- /dev/null +++ b/src/ini.c @@ -0,0 +1,269 @@ +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#include "ini.h" + +#if !INI_USE_STACK +#include +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Used by ini_parse_string() to keep track of string parsing state. */ +typedef struct { + const char* ptr; + size_t num_left; +} ini_parse_string_ctx; + +/* Strip whitespace chars off end of given string, in place. Return s. */ +static char* rstrip(char* s) +{ + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +static char* lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to null at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +static char* find_chars_or_comment(const char* s, const char* chars) +{ +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char*)s; +} + +/* Version of strncpy that ensures dest (size bytes) is null-terminated. */ +static char* strncpy0(char* dest, const char* src, size_t size) +{ + strncpy(dest, src, size); + dest[size - 1] = '\0'; + return dest; +} + +/* See documentation in header file. */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; + int max_line = INI_MAX_LINE; +#else + char* line; + int max_line = INI_INITIAL_ALLOC; +#endif +#if INI_ALLOW_REALLOC + char* new_line; + int offset; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char*)malloc(INI_INITIAL_ALLOC); + if (!line) { + return -2; + } +#endif + +#if INI_HANDLER_LINENO +#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) +#else +#define HANDLER(u, s, n, v) handler(u, s, n, v) +#endif + + /* Scan through stream line by line */ + while (reader(line, max_line, stream) != NULL) { +#if INI_ALLOW_REALLOC + offset = strlen(line); + while (offset == max_line - 1 && line[offset - 1] != '\n') { + max_line *= 2; + if (max_line > INI_MAX_LINE) + max_line = INI_MAX_LINE; + new_line = realloc(line, max_line); + if (!new_line) { + free(line); + return -2; + } + line = new_line; + if (reader(line + offset, max_line - offset, stream) == NULL) + break; + if (max_line >= INI_MAX_LINE) + break; + offset += strlen(line + offset); + } +#endif + + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (strchr(INI_START_COMMENT_PREFIXES, *start)) { + /* Start-of-line comment */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!HANDLER(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = end + 1; +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(value, NULL); + if (*end) + *end = '\0'; +#endif + value = lskip(value); + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!HANDLER(user, section, name, value) && !error) + error = lineno; + } + else if (!error) { + /* No '=' or ':' found on name[=:]value line */ + error = lineno; + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user) +{ + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +int ini_parse(const char* filename, ini_handler handler, void* user) +{ + FILE* file; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +/* An ini_reader function to read the next line from a string buffer. This + is the fgets() equivalent used by ini_parse_string(). */ +static char* ini_reader_string(char* str, int num, void* stream) { + ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; + const char* ctx_ptr = ctx->ptr; + size_t ctx_num_left = ctx->num_left; + char* strp = str; + char c; + + if (ctx_num_left == 0 || num < 2) + return NULL; + + while (num > 1 && ctx_num_left != 0) { + c = *ctx_ptr++; + ctx_num_left--; + *strp++ = c; + if (c == '\n') + break; + num--; + } + + *strp = '\0'; + ctx->ptr = ctx_ptr; + ctx->num_left = ctx_num_left; + return str; +} + +/* See documentation in header file. */ +int ini_parse_string(const char* string, ini_handler handler, void* user) { + ini_parse_string_ctx ctx; + + ctx.ptr = string; + ctx.num_left = strlen(string); + return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, + user); +} \ No newline at end of file