From f886773891f1cfe37f8824ed8e63230954e5c2d8 Mon Sep 17 00:00:00 2001 From: Sharvil Nanavati Date: Sun, 4 May 2014 21:33:52 -0700 Subject: [PATCH] Add a generic config parser for the INI file format. There are currently multiple INI parsers in bluedroid and they're special-purpose for the task at hand even though they parse the same format. This implementation is general-purpose, loosely coupled with the rest of bluedroid, and has unit tests to verify behaviour. Change-Id: I61caf416cc16d76b871cbf04f333c26894ab3fef --- osi/Android.mk | 5 +- osi/include/config.h | 78 +++++++++++++ osi/src/config.c | 278 +++++++++++++++++++++++++++++++++++++++++++++++ osi/test/config_test.cpp | 113 +++++++++++++++++++ 4 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 osi/include/config.h create mode 100644 osi/src/config.c create mode 100644 osi/test/config_test.cpp diff --git a/osi/Android.mk b/osi/Android.mk index c63e26658..23c43f79b 100644 --- a/osi/Android.mk +++ b/osi/Android.mk @@ -6,6 +6,7 @@ LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/include LOCAL_SRC_FILES := \ + ./src/config.c \ ./src/fixed_queue.c \ ./src/list.c \ ./src/semaphore.c @@ -13,7 +14,7 @@ LOCAL_SRC_FILES := \ LOCAL_CFLAGS := -std=c99 -Wall -Werror LOCAL_MODULE := libosi LOCAL_MODULE_TAGS := optional -LOCAL_SHARED_LIBRARIES := libc +LOCAL_SHARED_LIBRARIES := libc liblog LOCAL_MODULE_CLASS := STATIC_LIBRARIES include $(BUILD_STATIC_LIBRARY) @@ -26,11 +27,13 @@ LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/include LOCAL_SRC_FILES := \ + ./test/config_test.cpp \ ./test/list_test.cpp LOCAL_CFLAGS := -Wall -Werror LOCAL_MODULE := ositests LOCAL_MODULE_TAGS := tests +LOCAL_SHARED_LIBRARIES := liblog LOCAL_STATIC_LIBRARIES := libosi include $(BUILD_NATIVE_TEST) diff --git a/osi/include/config.h b/osi/include/config.h new file mode 100644 index 000000000..5e45003a5 --- /dev/null +++ b/osi/include/config.h @@ -0,0 +1,78 @@ +#pragma once + +// This module implements a configuration parser. Clients can query the +// contents of a configuration file through the interface provided here. +// The current implementation is read-only; mutations are only kept in +// memory. This parser supports the INI file format. + +// Implementation notes: +// - Key/value pairs that are not within a section are assumed to be under +// the |CONFIG_DEFAULT_SECTION| section. +// - Multiple sections with the same name will be merged as if they were in +// a single section. +// - Empty sections with no key/value pairs will be treated as if they do +// not exist. In other words, |config_has_section| will return false for +// empty sections. +// - Duplicate keys in a section will overwrite previous values. + +#include + +// The default section name to use if a key/value pair is not defined within +// a section. +#define CONFIG_DEFAULT_SECTION "Global" + +struct config_t; +typedef struct config_t config_t; + +// Loads the specified file and returns a handle to the config file. If there +// was a problem loading the file or allocating memory, this function returns +// NULL. Clients must call |config_free| on the returned handle when it is no +// longer required. |filename| must not be NULL and must point to a readable +// file on the filesystem. +config_t *config_new(const char *filename); + +// Frees resources associated with the config file. No further operations may +// be performed on the |config| object after calling this function. |config| +// may be NULL. +void config_free(config_t *config); + +// Returns true if the config file contains a section named |section|. If +// the section has no key/value pairs in it, this function will return false. +// |config| and |section| must not be NULL. +bool config_has_section(const config_t *config, const char *section); + +// Returns true if the config file has a key named |key| under |section|. +// Returns false otherwise. |config|, |section|, and |key| must not be NULL. +bool config_has_key(const config_t *config, const char *section, const char *key); + +// Returns the integral value for a given |key| in |section|. If |section| +// or |key| do not exist, or the value cannot be fully converted to an integer, +// this function returns |def_value|. |config|, |section|, and |key| must not +// be NULL. +int config_get_int(const config_t *config, const char *section, const char *key, int def_value); + +// Returns the boolean value for a given |key| in |section|. If |section| +// or |key| do not exist, or the value cannot be converted to a boolean, this +// function returns |def_value|. |config|, |section|, and |key| must not be NULL. +bool config_get_bool(const config_t *config, const char *section, const char *key, bool def_value); + +// Returns the string value for a given |key| in |section|. If |section| or +// |key| do not exist, this function returns |def_value|. The returned string +// is owned by the config module and must not be freed. |config|, |section|, +// and |key| must not be NULL. |def_value| may be NULL. +const char *config_get_string(const config_t *config, const char *section, const char *key, const char *def_value); + +// Sets an integral value for the |key| in |section|. If |key| or |section| do +// not already exist, this function creates them. |config|, |section|, and |key| +// must not be NULL. +void config_set_int(config_t *config, const char *section, const char *key, int value); + +// Sets a boolean value for the |key| in |section|. If |key| or |section| do +// not already exist, this function creates them. |config|, |section|, and |key| +// must not be NULL. +void config_set_bool(config_t *config, const char *section, const char *key, bool value); + +// Sets a string value for the |key| in |section|. If |key| or |section| do +// not already exist, this function creates them. |config|, |section|, |key|, and +// |value| must not be NULL. +void config_set_string(config_t *config, const char *section, const char *key, const char *value); diff --git a/osi/src/config.c b/osi/src/config.c new file mode 100644 index 000000000..f38c7f0c6 --- /dev/null +++ b/osi/src/config.c @@ -0,0 +1,278 @@ +#define LOG_TAG "bt_osi_config" + +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "list.h" + +typedef struct { + char *key; + char *value; +} entry_t; + +typedef struct { + char *name; + list_t *entries; +} section_t; + +struct config_t { + list_t *sections; +}; + +static void config_parse(FILE *fp, config_t *config); + +static section_t *section_new(const char *name); +static void section_free(void *ptr); +static section_t *section_find(const config_t *config, const char *section); + +static entry_t *entry_new(const char *key, const char *value); +static void entry_free(void *ptr); +static entry_t *entry_find(const config_t *config, const char *section, const char *key); + +config_t *config_new(const char *filename) { + assert(filename != NULL); + + FILE *fp = fopen(filename, "rt"); + if (!fp) { + ALOGE("%s unable to open file '%s': %s", __func__, filename, strerror(errno)); + return NULL; + } + + config_t *config = calloc(1, sizeof(config_t)); + if (!config) { + ALOGE("%s unable to allocate memory for config_t.", __func__); + fclose(fp); + return NULL; + } + + config->sections = list_new(section_free); + config_parse(fp, config); + + fclose(fp); + + return config; +} + +void config_free(config_t *config) { + if (!config) + return; + + list_free(config->sections); + free(config); +} + +bool config_has_section(const config_t *config, const char *section) { + assert(config != NULL); + assert(section != NULL); + + return (section_find(config, section) != NULL); +} + +bool config_has_key(const config_t *config, const char *section, const char *key) { + assert(config != NULL); + assert(section != NULL); + assert(key != NULL); + + return (entry_find(config, section, key) != NULL); +} + +int config_get_int(const config_t *config, const char *section, const char *key, int def_value) { + assert(config != NULL); + assert(section != NULL); + assert(key != NULL); + + entry_t *entry = entry_find(config, section, key); + if (!entry) + return def_value; + + char *endptr; + int ret = strtol(entry->value, &endptr, 0); + return (*endptr == '\0') ? ret : def_value; +} + +bool config_get_bool(const config_t *config, const char *section, const char *key, bool def_value) { + assert(config != NULL); + assert(section != NULL); + assert(key != NULL); + + entry_t *entry = entry_find(config, section, key); + if (!entry) + return def_value; + + if (!strcmp(entry->value, "true")) + return true; + if (!strcmp(entry->value, "false")) + return false; + + return def_value; +} + +const char *config_get_string(const config_t *config, const char *section, const char *key, const char *def_value) { + assert(config != NULL); + assert(section != NULL); + assert(key != NULL); + + entry_t *entry = entry_find(config, section, key); + if (!entry) + return def_value; + + return entry->value; +} + +void config_set_int(config_t *config, const char *section, const char *key, int value) { + assert(config != NULL); + assert(section != NULL); + assert(key != NULL); + + char value_str[32] = { 0 }; + sprintf(value_str, "%d", value); + config_set_string(config, section, key, value_str); +} + +void config_set_bool(config_t *config, const char *section, const char *key, bool value) { + assert(config != NULL); + assert(section != NULL); + assert(key != NULL); + + config_set_string(config, section, key, value ? "true" : "false"); +} + +void config_set_string(config_t *config, const char *section, const char *key, const char *value) { + section_t *sec = section_find(config, section); + if (!sec) { + sec = section_new(section); + list_append(config->sections, sec); + } + + for (const list_node_t *node = list_begin(sec->entries); node != list_end(sec->entries); node = list_next(node)) { + entry_t *entry = list_node(node); + if (!strcmp(entry->key, key)) { + free(entry->value); + entry->value = strdup(value); + return; + } + } + + entry_t *entry = entry_new(key, value); + list_append(sec->entries, entry); +} + +static char *trim(char *str) { + while (isspace(*str)) + ++str; + + if (!*str) + return str; + + char *end_str = str + strlen(str) - 1; + while (end_str > str && isspace(*end_str)) + --end_str; + + end_str[1] = '\0'; + return str; +} + +static void config_parse(FILE *fp, config_t *config) { + assert(fp != NULL); + assert(config != NULL); + + int line_num = 0; + char line[1024]; + char section[1024]; + strcpy(section, CONFIG_DEFAULT_SECTION); + + while (fgets(line, sizeof(line), fp)) { + char *line_ptr = trim(line); + ++line_num; + + // Skip blank and comment lines. + if (*line_ptr == '\0' || *line_ptr == '#') + continue; + + if (*line_ptr == '[') { + size_t len = strlen(line_ptr); + if (line_ptr[len - 1] != ']') { + ALOGD("%s unterminated section name on line %d.", __func__, line_num); + continue; + } + strncpy(section, line_ptr + 1, len - 2); + section[len - 2] = '\0'; + } else { + char *split = strchr(line_ptr, '='); + if (!split) { + ALOGD("%s no key/value separator found on line %d.", __func__, line_num); + continue; + } + + *split = '\0'; + config_set_string(config, section, trim(line_ptr), trim(split + 1)); + } + } +} + +static section_t *section_new(const char *name) { + section_t *section = calloc(1, sizeof(section_t)); + if (!section) + return NULL; + + section->name = strdup(name); + section->entries = list_new(entry_free); + return section; +} + +static void section_free(void *ptr) { + if (!ptr) + return; + + section_t *section = ptr; + free(section->name); + list_free(section->entries); +} + +static section_t *section_find(const config_t *config, const char *section) { + for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) { + section_t *sec = list_node(node); + if (!strcmp(sec->name, section)) + return sec; + } + + return NULL; +} + +static entry_t *entry_new(const char *key, const char *value) { + entry_t *entry = calloc(1, sizeof(entry_t)); + if (!entry) + return NULL; + + entry->key = strdup(key); + entry->value = strdup(value); + return entry; +} + +static void entry_free(void *ptr) { + if (!ptr) + return; + + entry_t *entry = ptr; + free(entry->key); + free(entry->value); +} + +static entry_t *entry_find(const config_t *config, const char *section, const char *key) { + section_t *sec = section_find(config, section); + if (!sec) + return NULL; + + for (const list_node_t *node = list_begin(sec->entries); node != list_end(sec->entries); node = list_next(node)) { + entry_t *entry = list_node(node); + if (!strcmp(entry->key, key)) + return entry; + } + + return NULL; +} diff --git a/osi/test/config_test.cpp b/osi/test/config_test.cpp new file mode 100644 index 000000000..1ce199dce --- /dev/null +++ b/osi/test/config_test.cpp @@ -0,0 +1,113 @@ +#include + +extern "C" { +#include "config.h" +} + +static const char CONFIG_FILE[] = "/data/local/tmp/config_test.conf"; +static const char CONFIG_FILE_CONTENT[] = +" \n\ +first_key=value \n\ + \n\ +# Device ID (DID) configuration \n\ +[DID] \n\ + \n\ +# Record Number: 1, 2 or 3 - maximum of 3 records \n\ +recordNumber = 1 \n\ + \n\ +# Primary Record - true or false (default) \n\ +# There can be only one primary record \n\ +primaryRecord = true \n\ + \n\ +# Vendor ID '0xFFFF' indicates no Device ID Service Record is present in the device \n\ +# 0x000F = Broadcom Corporation (default) \n\ +#vendorId = 0x000F \n\ + \n\ +# Vendor ID Source \n\ +# 0x0001 = Bluetooth SIG assigned Device ID Vendor ID value (default) \n\ +# 0x0002 = USB Implementer's Forum assigned Device ID Vendor ID value \n\ +#vendorIdSource = 0x0001 \n\ + \n\ +# Product ID & Product Version \n\ +# Per spec DID v1.3 0xJJMN for version is interpreted as JJ.M.N \n\ +# JJ: major version number, M: minor version number, N: sub-minor version number \n\ +# For example: 1200, v14.3.6 \n\ +productId = 0x1200 \n\ +version = 0x1111 \n\ + \n\ +# Optional attributes \n\ +#clientExecutableURL = \n\ +#serviceDescription = \n\ +#documentationURL = \n\ + \n\ +# Additional optional DID records. Bluedroid supports up to 3 records. \n\ +[DID] \n\ +[DID] \n\ +version = 0x1436 \n\ +"; + +class ConfigTest : public ::testing::Test { + protected: + virtual void SetUp() { + FILE *fp = fopen(CONFIG_FILE, "wt"); + fwrite(CONFIG_FILE_CONTENT, 1, sizeof(CONFIG_FILE_CONTENT), fp); + fclose(fp); + } +}; + +TEST_F(ConfigTest, config_new_no_file) { + config_t *config = config_new("/meow"); + EXPECT_TRUE(config == NULL); +} + +TEST_F(ConfigTest, config_new) { + config_t *config = config_new(CONFIG_FILE); + EXPECT_TRUE(config != NULL); + config_free(config); +} + +TEST_F(ConfigTest, config_free_null) { + config_free(NULL); +} + +TEST_F(ConfigTest, config_has_section) { + config_t *config = config_new(CONFIG_FILE); + EXPECT_TRUE(config_has_section(config, "DID")); + config_free(config); +} + +TEST_F(ConfigTest, config_has_key_in_default_section) { + config_t *config = config_new(CONFIG_FILE); + EXPECT_TRUE(config_has_key(config, CONFIG_DEFAULT_SECTION, "first_key")); + EXPECT_STREQ(config_get_string(config, CONFIG_DEFAULT_SECTION, "first_key", "meow"), "value"); + config_free(config); +} + +TEST_F(ConfigTest, config_has_keys) { + config_t *config = config_new(CONFIG_FILE); + EXPECT_TRUE(config_has_key(config, "DID", "recordNumber")); + EXPECT_TRUE(config_has_key(config, "DID", "primaryRecord")); + EXPECT_TRUE(config_has_key(config, "DID", "productId")); + EXPECT_TRUE(config_has_key(config, "DID", "version")); + config_free(config); +} + +TEST_F(ConfigTest, config_no_bad_keys) { + config_t *config = config_new(CONFIG_FILE); + EXPECT_FALSE(config_has_key(config, "DID_BAD", "primaryRecord")); + EXPECT_FALSE(config_has_key(config, "DID", "primaryRecord_BAD")); + EXPECT_FALSE(config_has_key(config, CONFIG_DEFAULT_SECTION, "primaryRecord")); + config_free(config); +} + +TEST_F(ConfigTest, config_get_int_version) { + config_t *config = config_new(CONFIG_FILE); + EXPECT_EQ(config_get_int(config, "DID", "version", 0), 0x1436); + config_free(config); +} + +TEST_F(ConfigTest, config_get_int_default) { + config_t *config = config_new(CONFIG_FILE); + EXPECT_EQ(config_get_int(config, "DID", "primaryRecord", 123), 123); + config_free(config); +} -- 2.11.0