Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d9a71ee3 authored by Sharvil Nanavati's avatar Sharvil Nanavati
Browse files

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
parent 2cfa10c5
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -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)
+78 −0
Original line number Diff line number Diff line
#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);
+278 −0
Original line number Diff line number Diff line
#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;
}
+113 −0
Original line number Diff line number Diff line
#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);
}