blob: ec9516268fca44b6013798651edb20393fc52872 [file] [log] [blame]
/******************************************************************************
*
* Copyright 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
#include "osi/include/config.h"
#include <base/files/file_util.h>
#include <base/logging.h>
#include <ctype.h>
#include <fcntl.h>
#include <libgen.h>
#include <log/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>
#include <sstream>
#include <type_traits>
#include "check.h"
void section_t::Set(std::string key, std::string value) {
for (entry_t& entry : entries) {
if (entry.key == key) {
entry.value = value;
return;
}
}
// add a new key to the section
entries.emplace_back(
entry_t{.key = std::move(key), .value = std::move(value)});
}
std::list<entry_t>::iterator section_t::Find(const std::string& key) {
return std::find_if(
entries.begin(), entries.end(),
[&key](const entry_t& entry) { return entry.key == key; });
}
bool section_t::Has(const std::string& key) {
return Find(key) != entries.end();
}
std::list<section_t>::iterator config_t::Find(const std::string& section) {
return std::find_if(
sections.begin(), sections.end(),
[&section](const section_t& sec) { return sec.name == section; });
}
bool config_t::Has(const std::string& key) {
return Find(key) != sections.end();
}
static bool config_parse(FILE* fp, config_t* config);
template <typename T,
class = typename std::enable_if<std::is_same<
config_t, typename std::remove_const<T>::type>::value>>
static auto section_find(T& config, const std::string& section) {
return std::find_if(
config.sections.begin(), config.sections.end(),
[&section](const section_t& sec) { return sec.name == section; });
}
static const entry_t* entry_find(const config_t& config,
const std::string& section,
const std::string& key) {
auto sec = section_find(config, section);
if (sec == config.sections.end()) return nullptr;
for (const entry_t& entry : sec->entries) {
if (entry.key == key) return &entry;
}
return nullptr;
}
std::unique_ptr<config_t> config_new_empty(void) {
return std::make_unique<config_t>();
}
std::unique_ptr<config_t> config_new(const char* filename) {
CHECK(filename != nullptr);
std::unique_ptr<config_t> config = config_new_empty();
FILE* fp = fopen(filename, "rt");
if (!fp) {
LOG(ERROR) << __func__ << ": unable to open file '" << filename
<< "': " << strerror(errno);
return nullptr;
}
if (!config_parse(fp, config.get())) {
config.reset();
}
fclose(fp);
return config;
}
std::string checksum_read(const char* filename) {
base::FilePath path(filename);
if (!base::PathExists(path)) {
LOG(ERROR) << __func__ << ": unable to locate file '" << filename << "'";
return "";
}
std::string encrypted_hash;
if (!base::ReadFileToString(path, &encrypted_hash)) {
LOG(ERROR) << __func__ << ": unable to read file '" << filename << "'";
}
return encrypted_hash;
}
std::unique_ptr<config_t> config_new_clone(const config_t& src) {
std::unique_ptr<config_t> ret = config_new_empty();
for (const section_t& sec : src.sections) {
for (const entry_t& entry : sec.entries) {
config_set_string(ret.get(), sec.name, entry.key, entry.value);
}
}
return ret;
}
bool config_has_section(const config_t& config, const std::string& section) {
return (section_find(config, section) != config.sections.end());
}
bool config_has_key(const config_t& config, const std::string& section,
const std::string& key) {
return (entry_find(config, section, key) != nullptr);
}
int config_get_int(const config_t& config, const std::string& section,
const std::string& key, int def_value) {
const entry_t* entry = entry_find(config, section, key);
if (!entry) return def_value;
char* endptr;
int ret = strtol(entry->value.c_str(), &endptr, 0);
return (*endptr == '\0') ? ret : def_value;
}
uint64_t config_get_uint64(const config_t& config, const std::string& section,
const std::string& key, uint64_t def_value) {
const entry_t* entry = entry_find(config, section, key);
if (!entry) return def_value;
char* endptr;
uint64_t ret = strtoull(entry->value.c_str(), &endptr, 0);
return (*endptr == '\0') ? ret : def_value;
}
bool config_get_bool(const config_t& config, const std::string& section,
const std::string& key, bool def_value) {
const entry_t* entry = entry_find(config, section, key);
if (!entry) return def_value;
if (entry->value == "true") return true;
if (entry->value == "false") return false;
return def_value;
}
const std::string* config_get_string(const config_t& config,
const std::string& section,
const std::string& key,
const std::string* def_value) {
const entry_t* entry = entry_find(config, section, key);
if (!entry) return def_value;
return &entry->value;
}
void config_set_int(config_t* config, const std::string& section,
const std::string& key, int value) {
config_set_string(config, section, key, std::to_string(value));
}
void config_set_uint64(config_t* config, const std::string& section,
const std::string& key, uint64_t value) {
config_set_string(config, section, key, std::to_string(value));
}
void config_set_bool(config_t* config, const std::string& section,
const std::string& key, bool value) {
config_set_string(config, section, key, value ? "true" : "false");
}
void config_set_string(config_t* config, const std::string& section,
const std::string& key, const std::string& value) {
CHECK(config);
auto sec = section_find(*config, section);
if (sec == config->sections.end()) {
config->sections.emplace_back(section_t{.name = section});
sec = std::prev(config->sections.end());
}
std::string value_no_newline;
size_t newline_position = value.find('\n');
if (newline_position != std::string::npos) {
value_no_newline = value.substr(0, newline_position);
} else {
value_no_newline = value;
}
for (entry_t& entry : sec->entries) {
if (entry.key == key) {
entry.value = value_no_newline;
return;
}
}
sec->entries.emplace_back(entry_t{.key = key, .value = value_no_newline});
}
bool config_remove_section(config_t* config, const std::string& section) {
CHECK(config);
auto sec = section_find(*config, section);
if (sec == config->sections.end()) return false;
config->sections.erase(sec);
return true;
}
bool config_remove_key(config_t* config, const std::string& section,
const std::string& key) {
CHECK(config);
auto sec = section_find(*config, section);
if (sec == config->sections.end()) return false;
for (auto entry = sec->entries.begin(); entry != sec->entries.end();
++entry) {
if (entry->key == key) {
sec->entries.erase(entry);
return true;
}
}
return false;
}
bool config_save(const config_t& config, const std::string& filename) {
CHECK(!filename.empty());
// Steps to ensure content of config file gets to disk:
//
// 1) Open and write to temp file (e.g. bt_config.conf.new).
// 2) Flush the stream buffer to the temp file.
// 3) Sync the temp file to disk with fsync().
// 4) Rename temp file to actual config file (e.g. bt_config.conf).
// This ensures atomic update.
// 5) Sync directory that has the conf file with fsync().
// This ensures directory entries are up-to-date.
int dir_fd = -1;
FILE* fp = nullptr;
std::stringstream serialized;
// Build temp config file based on config file (e.g. bt_config.conf.new).
const std::string temp_filename = filename + ".new";
// Extract directory from file path (e.g. /data/misc/bluedroid).
const std::string directoryname = base::FilePath(filename).DirName().value();
if (directoryname.empty()) {
LOG(ERROR) << __func__ << ": error extracting directory from '" << filename
<< "': " << strerror(errno);
goto error;
}
dir_fd = open(directoryname.c_str(), O_RDONLY);
if (dir_fd < 0) {
LOG(ERROR) << __func__ << ": unable to open dir '" << directoryname
<< "': " << strerror(errno);
goto error;
}
fp = fopen(temp_filename.c_str(), "wt");
if (!fp) {
LOG(ERROR) << __func__ << ": unable to write to file '" << temp_filename
<< "': " << strerror(errno);
goto error;
}
for (const section_t& section : config.sections) {
serialized << "[" << section.name << "]" << std::endl;
for (const entry_t& entry : section.entries)
serialized << entry.key << " = " << entry.value << std::endl;
serialized << std::endl;
}
if (fprintf(fp, "%s", serialized.str().c_str()) < 0) {
LOG(ERROR) << __func__ << ": unable to write to file '" << temp_filename
<< "': " << strerror(errno);
goto error;
}
// Flush the stream buffer to the temp file.
if (fflush(fp) < 0) {
LOG(ERROR) << __func__ << ": unable to write flush buffer to file '"
<< temp_filename << "': " << strerror(errno);
goto error;
}
// Sync written temp file out to disk. fsync() is blocking until data makes it
// to disk.
if (fsync(fileno(fp)) < 0) {
LOG(WARNING) << __func__ << ": unable to fsync file '" << temp_filename
<< "': " << strerror(errno);
}
if (fclose(fp) == EOF) {
LOG(ERROR) << __func__ << ": unable to close file '" << temp_filename
<< "': " << strerror(errno);
goto error;
}
fp = nullptr;
// Change the file's permissions to Read/Write by User and Group
if (chmod(temp_filename.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) ==
-1) {
LOG(ERROR) << __func__ << ": unable to change file permissions '"
<< filename << "': " << strerror(errno);
goto error;
}
// Rename written temp file to the actual config file.
if (rename(temp_filename.c_str(), filename.c_str()) == -1) {
LOG(ERROR) << __func__ << ": unable to commit file '" << filename
<< "': " << strerror(errno);
goto error;
}
// This should ensure the directory is updated as well.
if (fsync(dir_fd) < 0) {
LOG(WARNING) << __func__ << ": unable to fsync dir '" << directoryname
<< "': " << strerror(errno);
}
if (close(dir_fd) < 0) {
LOG(ERROR) << __func__ << ": unable to close dir '" << directoryname
<< "': " << strerror(errno);
goto error;
}
return true;
error:
// This indicates there is a write issue. Unlink as partial data is not
// acceptable.
unlink(temp_filename.c_str());
if (fp) fclose(fp);
if (dir_fd != -1) close(dir_fd);
return false;
}
bool checksum_save(const std::string& checksum, const std::string& filename) {
CHECK(!checksum.empty()) << __func__ << ": checksum cannot be empty";
CHECK(!filename.empty()) << __func__ << ": filename cannot be empty";
// Steps to ensure content of config checksum file gets to disk:
//
// 1) Open and write to temp file (e.g.
// bt_config.conf.encrypted-checksum.new). 2) Sync the temp file to disk with
// fsync(). 3) Rename temp file to actual config checksum file (e.g.
// bt_config.conf.encrypted-checksum).
// This ensures atomic update.
// 4) Sync directory that has the conf file with fsync().
// This ensures directory entries are up-to-date.
FILE* fp = nullptr;
int dir_fd = -1;
// Build temp config checksum file based on config checksum file (e.g.
// bt_config.conf.encrypted-checksum.new).
const std::string temp_filename = filename + ".new";
base::FilePath path(temp_filename);
// Extract directory from file path (e.g. /data/misc/bluedroid).
const std::string directoryname = base::FilePath(filename).DirName().value();
if (directoryname.empty()) {
LOG(ERROR) << __func__ << ": error extracting directory from '" << filename
<< "': " << strerror(errno);
goto error2;
}
dir_fd = open(directoryname.c_str(), O_RDONLY);
if (dir_fd < 0) {
LOG(ERROR) << __func__ << ": unable to open dir '" << directoryname
<< "': " << strerror(errno);
goto error2;
}
if (base::WriteFile(path, checksum.data(), checksum.size()) !=
(int)checksum.size()) {
LOG(ERROR) << __func__ << ": unable to write file '" << filename.c_str();
goto error2;
}
fp = fopen(temp_filename.c_str(), "rb");
if (!fp) {
LOG(ERROR) << __func__ << ": unable to write to file '" << temp_filename
<< "': " << strerror(errno);
goto error2;
}
// Sync written temp file out to disk. fsync() is blocking until data makes it
// to disk.
if (fsync(fileno(fp)) < 0) {
LOG(WARNING) << __func__ << ": unable to fsync file '" << temp_filename
<< "': " << strerror(errno);
}
if (fclose(fp) == EOF) {
LOG(ERROR) << __func__ << ": unable to close file '" << temp_filename
<< "': " << strerror(errno);
goto error2;
}
fp = nullptr;
// Change the file's permissions to Read/Write by User and Group
if (chmod(temp_filename.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) ==
-1) {
LOG(ERROR) << __func__ << ": unable to change file permissions '"
<< filename << "': " << strerror(errno);
goto error2;
}
// Rename written temp file to the actual config file.
if (rename(temp_filename.c_str(), filename.c_str()) == -1) {
LOG(ERROR) << __func__ << ": unable to commit file '" << filename
<< "': " << strerror(errno);
goto error2;
}
// This should ensure the directory is updated as well.
if (fsync(dir_fd) < 0) {
LOG(WARNING) << __func__ << ": unable to fsync dir '" << directoryname
<< "': " << strerror(errno);
}
if (close(dir_fd) < 0) {
LOG(ERROR) << __func__ << ": unable to close dir '" << directoryname
<< "': " << strerror(errno);
goto error2;
}
return true;
error2:
// This indicates there is a write issue. Unlink as partial data is not
// acceptable.
unlink(temp_filename.c_str());
if (fp) fclose(fp);
if (dir_fd != -1) close(dir_fd);
return false;
}
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 bool config_parse(FILE* fp, config_t* config) {
CHECK(fp != nullptr);
CHECK(config != nullptr);
int line_num = 0;
char line[4096];
char section[4096];
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] != ']') {
VLOG(1) << __func__ << ": unterminated section name on line "
<< line_num;
return false;
}
strncpy(section, line_ptr + 1, len - 2); // NOLINT (len < 4096)
section[len - 2] = '\0';
} else {
char* split = strchr(line_ptr, '=');
if (!split) {
VLOG(1) << __func__ << ": no key/value separator found on line "
<< line_num;
return false;
}
*split = '\0';
config_set_string(config, section, trim(line_ptr), trim(split + 1));
}
}
return true;
}