OSDN Git Service

Add a generic config parser for the INI file format.
authorSharvil Nanavati <sharvil@google.com>
Mon, 5 May 2014 04:33:52 +0000 (21:33 -0700)
committerSharvil Nanavati <sharvil@google.com>
Sat, 31 May 2014 03:41:46 +0000 (20:41 -0700)
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
osi/include/config.h [new file with mode: 0644]
osi/src/config.c [new file with mode: 0644]
osi/test/config_test.cpp [new file with mode: 0644]

index c63e266..23c43f7 100644 (file)
@@ -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 (file)
index 0000000..5e45003
--- /dev/null
@@ -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 <stdbool.h>
+
+// 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 (file)
index 0000000..f38c7f0
--- /dev/null
@@ -0,0 +1,278 @@
+#define LOG_TAG "bt_osi_config"
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <utils/Log.h>
+
+#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 (file)
index 0000000..1ce199d
--- /dev/null
@@ -0,0 +1,113 @@
+#include <gtest/gtest.h>
+
+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);
+}