diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..009d0ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*~ +*.swp +*.swo +.*.sw? +.DS_Store +build +.vscode +**/Makefile +**/CMakeCache.txt +**/cmake_install.cmake +**/CMakeFiles \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..da3a515 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.16) +project(sagent) + +# Force build type to Release build. +set(CMAKE_BUILD_TYPE Release) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY build) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY build) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY build) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-result -Wreturn-type") + +add_executable(sagent + src/util/util.cpp + src/conf.cpp + src/salog.cpp + src/sqlite.cpp + src/main.cpp +) + +target_link_libraries(sagent + sqlite3 + ${CMAKE_DL_LIBS} # Needed for stacktrace support +) + +target_precompile_headers(sagent PUBLIC src/pchheader.hpp) diff --git a/README.md b/README.md index 0ca446a..9c8112d 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ -# Introduction -TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project. +# Sashimono Agent -# Getting Started -TODO: Guide users through getting your code up and running on their own system. In this section you can talk about: -1. Installation process -2. Software dependencies -3. Latest releases -4. API references +## What's here? +*In development* -# Build and Test -TODO: Describe and show how to build your code and run the tests. +A C++ version of sashimono agent -# Contribute -TODO: Explain how other users and developers can contribute to make your code better. +## Libraries -If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files: -- [ASP.NET Core](https://github.com/aspnet/Home) -- [Visual Studio Code](https://github.com/Microsoft/vscode) -- [Chakra Core](https://github.com/Microsoft/ChakraCore) \ No newline at end of file +## Setting up Sashimono Agent environment +Run the setup script located at the repo root (tested on Ubuntu 18.04). +``` +./dev-setup.sh +``` + +## Build Sashimono Agent +1. Run `cmake .` (You only have to do this once) +1. Run `make` (Sashimono agent binary will be created as `./build/sagent`) + +## Code structure +Code is divided into subsystems via namespaces. + +**conf::** Handles configuration. Loads and holds the central configuration object. Used by most of the subsystems. + +**util::** Contains shared data structures/helper functions used by multiple subsystems. + +**sqlite::** Contains sqlite database management related helper functions. \ No newline at end of file diff --git a/dev-setup.sh b/dev-setup.sh new file mode 100755 index 0000000..4f8e376 --- /dev/null +++ b/dev-setup.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Usage ./dev-setup.sh +# Sashimono agent build environment setup script. + +set -e # exit on error + +sudo apt-get update +sudo apt-get install -y build-essential libssl-dev + +workdir=~/sagent-setup + +mkdir $workdir +pushd $workdir > /dev/null 2>&1 + +# CMAKE +cmake=cmake-3.16.0-rc3-Linux-x86_64 +wget https://github.com/Kitware/CMake/releases/download/v3.16.0-rc3/$cmake.tar.gz +tar -zxvf $cmake.tar.gz +sudo cp -r $cmake/bin/* /usr/local/bin/ +sudo cp -r $cmake/share/* /usr/local/share/ +rm $cmake.tar.gz && rm -r $cmake + +# jsoncons +wget https://github.com/danielaparker/jsoncons/archive/v0.153.3.tar.gz +tar -zxvf v0.153.3.tar.gz +pushd jsoncons-0.153.3 > /dev/null 2>&1 +sudo cp -r include/jsoncons /usr/local/include/ +sudo mkdir -p /usr/local/include/jsoncons_ext/ +sudo cp -r include/jsoncons_ext/bson /usr/local/include/jsoncons_ext/ +popd > /dev/null 2>&1 +rm v0.153.3.tar.gz && rm -r jsoncons-0.153.3 + +# Sqlite +sudo apt-get install -y sqlite3 libsqlite3-dev + +# Plog +wget https://github.com/SergiusTheBest/plog/archive/1.1.5.tar.gz +tar -zxvf 1.1.5.tar.gz +pushd plog-1.1.5 > /dev/null 2>&1 +sudo cp -r include/plog /usr/local/include/ +popd > /dev/null 2>&1 +rm 1.1.5.tar.gz && rm -r plog-1.1.5 + +# Update linker library cache. +sudo ldconfig + +# Pop workdir +popd > /dev/null 2>&1 +rm -r $workdir + +# Build sagent +cmake . +make \ No newline at end of file diff --git a/src/conf.cpp b/src/conf.cpp new file mode 100644 index 0000000..3807030 --- /dev/null +++ b/src/conf.cpp @@ -0,0 +1,334 @@ +#include "conf.hpp" +#include "util/util.hpp" + +namespace conf +{ + // Global contract context struct exposed to the application. + sa_context ctx; + + // Global configuration struct exposed to the application. + sa_config cfg; + + constexpr int FILE_PERMS = 0644; + + bool init_success = false; + + /** + * Loads and initializes the config for execution. Must be called once during application startup. + * @return 0 for success. -1 for failure. + */ + int init() + { + if (validate_dir_paths() == -1 || + read_config(cfg) == -1 || + validate_config(cfg) == -1) + return -1; + + init_success = true; + return 0; + } + + /** + * Cleanup any resources. + */ + void deinit() + { + if (init_success) + { + // Deinit here. + } + } + + /** + * Create config here. + * @return 0 for success. -1 for failure. + */ + int create() + { + if (util::is_dir_exists(ctx.config_dir)) + { + if (util::is_file_exists(ctx.config_file)) + { + std::cerr << "Config file already exists. Cannot create config at the same location.\n"; + return -1; + } + } + else + { + // Recursivly create contract directory. Return an error if unable to create + if (util::create_dir_tree_recursive(ctx.config_dir) == -1 || + util::create_dir_tree_recursive(ctx.log_dir) == -1) + { + std::cerr << "ERROR: unable to create directories.\n"; + return -1; + } + } + + //Create config file with default settings. + //We populate the in-memory struct with default settings and then save it to the file. + { + sa_config cfg = {}; + + cfg.version = "0.0.1"; + cfg.log.max_file_count = 50; + cfg.log.max_mbytes_per_file = 10; + cfg.log.log_level = "inf"; + cfg.log.loggers.emplace("console"); + cfg.log.loggers.emplace("file"); + + //Save the default settings into the config file. + if (write_config(cfg) != 0) + return -1; + } + + std::cout << "Config file created at " << ctx.config_file << std::endl; + + return 0; + } + + /** + * Updates the context with directory paths based on provided base directory. + * This is called after parsing SA command line arg in order to populate the ctx. + * @param basedir Path to base directory. + */ + void set_dir_paths(std::string basedir) + { + if (basedir.empty()) + { + // This code branch will never execute the way main is currently coded, but it might change in future + std::cerr << "Base directory must be specified\n"; + exit(1); + } + + // Resolving the path through realpath will remove any trailing slash if present + // Set config directory to the parent of the exec binary. + basedir = dirname((char *)util::realpath(basedir).data()); + ctx.config_dir = basedir + "/cfg"; + ctx.config_file = ctx.config_dir + "/sa.cfg"; + ctx.log_dir = basedir + "/log"; + } + + /** + * Checks for the existence of all contract sub directories. + * @return 0 for successful validation. -1 for failure. + */ + int validate_dir_paths() + { + const std::string paths[2] = { + ctx.config_file, + ctx.log_dir}; + + for (const std::string &path : paths) + { + if (!util::is_file_exists(path) && !util::is_dir_exists(path)) + { + std::cerr << path << " does not exist.\n"; + return -1; + } + } + + return 0; + } + + /** + * Reads the config file on disk and populates the in-memory 'cfg' struct. + * @param cfg Config to populate. + * @return 0 for successful loading of config. -1 for failure. + */ + int read_config(sa_config &cfg) + { + int fd = open(ctx.config_file.data(), O_RDWR, 444); + if (fd == -1) + return -1; + + // Read the config file into json document object. + std::string buf; + if (util::read_from_fd(fd, buf) == -1) + { + std::cerr << "Error reading from the config file. " << errno << '\n'; + return -1; + } + + jsoncons::ojson d; + try + { + d = jsoncons::ojson::parse(buf, jsoncons::strict_json_parsing()); + } + catch (const std::exception &e) + { + std::cerr << "Invalid config file format. " << e.what() << '\n'; + return -1; + } + buf.clear(); + + try + { + // Check whether the version is specified. + cfg.version = d["version"].as(); + if (cfg.version.empty()) + { + std::cerr << "Config version missing.\n"; + return -1; + } + } + catch (const std::exception &e) + { + std::cerr << "Required config field version missing at " << ctx.config_file << std::endl; + return -1; + } + + std::string jpath; + + // log + { + jpath = "log"; + + try + { + const jsoncons::ojson &log = d["log"]; + cfg.log.log_level = log["log_level"].as(); + cfg.log.log_level_type = get_loglevel_type(cfg.log.log_level); + + cfg.log.max_mbytes_per_file = log["max_mbytes_per_file"].as(); + cfg.log.max_file_count = log["max_file_count"].as(); + cfg.log.loggers.clear(); + for (auto &v : log["loggers"].array_range()) + cfg.log.loggers.emplace(v.as()); + } + catch (const std::exception &e) + { + print_missing_field_error(jpath, e); + return -1; + } + } + + return 0; + } + + /** + * Saves the provided 'cfg' struct into the config file. + * @param cfg Config to write. + * @return 0 for successful save. -1 for failure. + */ + int write_config(const sa_config &cfg) + { + // Popualte json document with 'cfg' values. + // ojson is used instead of json to preserve insertion order. + jsoncons::ojson d; + d.insert_or_assign("version", cfg.version); + + // Log configs. + { + jsoncons::ojson log_config; + log_config.insert_or_assign("log_level", cfg.log.log_level); + log_config.insert_or_assign("max_mbytes_per_file", cfg.log.max_mbytes_per_file); + log_config.insert_or_assign("max_file_count", cfg.log.max_file_count); + + jsoncons::ojson loggers(jsoncons::json_array_arg); + for (std::string_view logger : cfg.log.loggers) + { + loggers.push_back(logger); + } + log_config.insert_or_assign("loggers", loggers); + d.insert_or_assign("log", log_config); + } + + return write_json_file(ctx.config_file, d); + } + + /** + * Writes the given json doc to a file. + * @param file_path Path to the file. + * @param d Json object. + * @return 0 on success. -1 on failure. + */ + int write_json_file(const std::string &file_path, const jsoncons::ojson &d) + { + std::string json; + // Convert json object to a string. + try + { + jsoncons::json_options options; + options.object_array_line_splits(jsoncons::line_split_kind::multi_line); + options.spaces_around_comma(jsoncons::spaces_option::no_spaces); + std::ostringstream os; + os << jsoncons::pretty_print(d, options); + json = os.str(); + os.clear(); + } + catch (const std::exception &e) + { + std::cerr << "Converting json to string failed. " << file_path << std::endl; + return -1; + } + + // O_TRUNC flag is used to trucate existing content from the file. + const int fd = open(file_path.data(), O_CREAT | O_RDWR | O_TRUNC, FILE_PERMS); + if (fd == -1 || write(fd, json.data(), json.size()) == -1) + { + std::cerr << "Writing file failed. " << file_path << std::endl; + if (fd != -1) + close(fd); + return -1; + } + close(fd); + return 0; + } + + /** + * Convert string to Log Severity enum type. + * @param severity log severity code. + * @return log severity type. + */ + LOG_SEVERITY get_loglevel_type(std::string_view severity) + { + if (severity == "dbg") + return LOG_SEVERITY::DEBUG; + else if (severity == "wrn") + return LOG_SEVERITY::WARN; + else if (severity == "inf") + return LOG_SEVERITY::INFO; + else + return LOG_SEVERITY::ERROR; + } + + /** + * Prints the config json parsing field missing error. + * @param jpath Json path of the feild. + * @param e Exception. + */ + void print_missing_field_error(std::string_view jpath, const std::exception &e) + { + // Extract field name from jsoncons exception message. + std::cerr << "Config validation error: " << e.what() << " in '" << jpath << "' section at " << ctx.config_file << std::endl; + } + + /** + * Validates the 'cfg' struct for invalid values. + * @param cfg Config to validate. + * @return 0 for successful validation. -1 for failure. + */ + int validate_config(const sa_config &cfg) + { + // Other required fields. + + bool fields_invalid = false; + fields_invalid |= cfg.log.log_level.empty() && std::cerr << "Invalid value for loglevel.\n"; + + if (fields_invalid) + { + std::cerr << "Invalid configuration values at " << ctx.config_file << std::endl; + return -1; + } + + const std::unordered_set valid_loglevels({"dbg", "inf", "wrn", "err"}); + if (valid_loglevels.count(cfg.log.log_level) != 1) + { + std::cerr << "Invalid loglevel configured. Valid values: dbg|inf|wrn|err\n"; + return -1; + } + + return 0; + } + +} \ No newline at end of file diff --git a/src/conf.hpp b/src/conf.hpp new file mode 100644 index 0000000..0ae6d62 --- /dev/null +++ b/src/conf.hpp @@ -0,0 +1,73 @@ +#ifndef _SA_CONF_ +#define _SA_CONF_ + +#include "pchheader.hpp" + +namespace conf +{ + // Log severity levels used in Sashimono agent. + enum LOG_SEVERITY + { + DEBUG, + INFO, + WARN, + ERROR + }; + + struct log_config + { + std::string log_level; // Log severity level (dbg, inf, wrn, wrr) + LOG_SEVERITY log_level_type; // Log severity level enum (debug, info, warn, error) + std::unordered_set loggers; // List of enabled loggers (console, file) + size_t max_mbytes_per_file = 0; // Max MB size of a single log file. + size_t max_file_count = 0; // Max no. of log files to keep. + }; + + struct sa_config + { + std::string version; + log_config log; + }; + + struct sa_context + { + std::string command; // The CLI command issued to launch Sashimono agent + + std::string config_dir; // Config dir full path. + std::string config_file; // Full path to the config file. + std::string log_dir; // Log directory full path. + }; + + // Global context struct exposed to the application. + // Other modules will access context values via this. + extern sa_context ctx; + + // Global configuration struct exposed to the application. + // Other modules will access config values via this. + extern sa_config cfg; + + int init(); + + void deinit(); + + int create(); + + void set_dir_paths(std::string basedir); + + int validate_dir_paths(); + + int read_config(sa_config &cfg); + + int write_config(const sa_config &cfg); + + int write_json_file(const std::string &file_path, const jsoncons::ojson &d); + + LOG_SEVERITY get_loglevel_type(std::string_view severity); + + void print_missing_field_error(std::string_view jpath, const std::exception &e); + + int validate_config(const sa_config &cfg); + +} + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..d67aacf --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,104 @@ +/** + Entry point for Sashimono +**/ +#include "pchheader.hpp" +#include "conf.hpp" +#include "sqlite.hpp" +#include "salog.hpp" + +/** + * Parses CLI args and extracts sashimono agent command and parameters given. + * @param argc Argument count. + * @param argv Arguments. + * @returns 0 on success, -1 on error. + */ +int parse_cmd(int argc, char **argv) +{ + conf::ctx.command = argv[1]; + if (argc == 2 && //We get working dir as an arg anyway. So we need to check for ==2 args. + (conf::ctx.command == "new" || conf::ctx.command == "run" || conf::ctx.command == "version")) + { + // We populate the global contract ctx with the detected command. + conf::set_dir_paths(argv[0]); + return 0; + } + + // If all extractions fail display help message. + std::cerr << "Arguments mismatch.\n"; + std::cout << "Usage:\n"; + std::cout << "sagent version\n"; + std::cout << "sagent (command = run | new | rekey)\n"; + std::cout << "Example: sagent run\n"; + + return -1; +} + +/** + * Performs any cleanup on graceful application termination. + */ +void deinit() +{ + conf::deinit(); +} + +int main(int argc, char **argv) +{ + // Extract the CLI args + // This call will populate conf::ctx + if (parse_cmd(argc, argv) != 0) + return -1; + + if (conf::ctx.command == "new") + { + // This will create a new config. + if (conf::create() != 0) + return -1; + } + else + { + if (conf::init() != 0) + return -1; + + salog::init(); // Initialize logger for SA. + + if (conf::ctx.command == "run") + { + LOG_INFO << "Sashimono agent started. Version : " << conf::cfg.version << " Log level : " << conf::cfg.log.log_level; + + // Run the program. + + sqlite3 *db = NULL; + const char *path = "db.sqlite"; + + if (sqlite::open_db(path, &db, true) == -1) + { + LOG_ERROR << "Error opening database"; + return -1; + } + LOG_INFO << "Database " << path << " opened successfully"; + + const std::vector column_info{ + sqlite::table_column_info("VERSION", sqlite::COLUMN_DATA_TYPE::TEXT)}; + + if (create_table(db, "SA_VERSION", column_info) == -1) + return -1; + + if (sqlite::insert_row(db, "SA_VERSION", "VERSION", "\"0.0.0\"") == -1) + return -1; + + if (sqlite::close_db(&db) == -1) + { + LOG_ERROR << "Error closing database"; + return -1; + } + } + else if (conf::ctx.command == "version") + // Print the version + LOG_INFO << "Sashimono Agent " << conf::cfg.version; + + deinit(); + } + + LOG_INFO << "sashimono agent exited normally."; + return 0; +} diff --git a/src/pchheader.hpp b/src/pchheader.hpp new file mode 100644 index 0000000..df888d0 --- /dev/null +++ b/src/pchheader.hpp @@ -0,0 +1,19 @@ +#ifndef _SA_PCHHEADER_ +#define _SA_PCHHEADER_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif \ No newline at end of file diff --git a/src/salog.cpp b/src/salog.cpp new file mode 100644 index 0000000..e1e9f8f --- /dev/null +++ b/src/salog.cpp @@ -0,0 +1,90 @@ +#include "pchheader.hpp" +#include "conf.hpp" +#include "salog.hpp" + +namespace salog +{ + class plog_formatter; + + // Custom formatter adopted from: + // https://github.com/SergiusTheBest/plog/blob/master/include/plog/Formatters/TxtFormatter.h + class plog_formatter + { + public: + static plog::util::nstring header() + { + return plog::util::nstring(); + } + + static inline const char *severity_to_string(plog::Severity severity) + { + switch (severity) + { + case plog::Severity::fatal: + return "fat"; + case plog::Severity::error: + return "err"; + case plog::Severity::warning: + return "wrn"; + case plog::Severity::info: + return "inf"; + case plog::Severity::debug: + return "dbg"; + case plog::Severity::verbose: + return "ver"; + default: + return "def"; + } + } + + static plog::util::nstring format(const plog::Record &record) + { + tm t; + plog::util::localtime_s(&t, &record.getTime().time); // local time + + plog::util::nostringstream ss; + ss << t.tm_year + 1900 << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_mon + 1 << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_mday << PLOG_NSTR(" "); + ss << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_hour << PLOG_NSTR(":") + << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_min << PLOG_NSTR(":") + << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_sec << PLOG_NSTR(" "); + // Uncomment for millseconds. + // << std::setfill(PLOG_NSTR('0')) << std::setw(3) << record.getTime().millitm << PLOG_NSTR(" "); + + ss << PLOG_NSTR("[") << severity_to_string(record.getSeverity()) << PLOG_NSTR("][sa] "); + ss << record.getMessage() << PLOG_NSTR("\n"); + + return ss.str(); + } + }; + + void init() + { + plog::Severity level; + + if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::DEBUG) + level = plog::Severity::debug; + else if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::INFO) + level = plog::Severity::info; + else if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::WARN) + level = plog::Severity::warning; + else + level = plog::Severity::error; + + const std::string trace_file = conf::ctx.log_dir + "/sa.log"; + static plog::RollingFileAppender fileAppender(trace_file.c_str(), conf::cfg.log.max_mbytes_per_file * 1024 * 1024, conf::cfg.log.max_file_count); + static plog::ColorConsoleAppender consoleAppender; + + plog::Logger<0> &logger = plog::init(level); + + // Take decision to append logger for file / console or both. + if (conf::cfg.log.loggers.count("console") == 1) + { + logger.addAppender(&consoleAppender); + } + + if (conf::cfg.log.loggers.count("file") == 1) + { + logger.addAppender(&fileAppender); + } + } +} // namespace salog diff --git a/src/salog.hpp b/src/salog.hpp new file mode 100644 index 0000000..75bfe97 --- /dev/null +++ b/src/salog.hpp @@ -0,0 +1,9 @@ +#ifndef _SA_SALOG_ +#define _SA_SALOG_ + +namespace salog +{ + void init(); +} // namespace salog + +#endif \ No newline at end of file diff --git a/src/sqlite.cpp b/src/sqlite.cpp new file mode 100644 index 0000000..2122dfd --- /dev/null +++ b/src/sqlite.cpp @@ -0,0 +1,257 @@ +#include "sqlite.hpp" +#include "salog.hpp" + +namespace sqlite +{ + constexpr const char *COLUMN_DATA_TYPES[]{"INT", "TEXT", "BLOB"}; + constexpr const char *CREATE_TABLE = "CREATE TABLE IF NOT EXISTS "; + constexpr const char *CREATE_INDEX = "CREATE INDEX "; + constexpr const char *CREATE_UNIQUE_INDEX = "CREATE UNIQUE INDEX "; + constexpr const char *BEGIN_TRANSACTION = "BEGIN TRANSACTION;"; + constexpr const char *COMMIT_TRANSACTION = "COMMIT;"; + constexpr const char *ROLLBACK_TRANSACTION = "ROLLBACK;"; + constexpr const char *INSERT_INTO = "INSERT INTO "; + constexpr const char *PRIMARY_KEY = "PRIMARY KEY"; + constexpr const char *NOT_NULL = "NOT NULL"; + constexpr const char *VALUES = "VALUES"; + constexpr const char *SELECT_ALL = "SELECT * FROM "; + constexpr const char *SQLITE_MASTER = "sqlite_master"; + constexpr const char *WHERE = " WHERE "; + constexpr const char *AND = " AND "; + + /** + * Opens a connection to a given databse and give the db pointer. + * @param db_name Database name to be connected. + * @param db Pointer to the db pointer which is to be connected and pointed. + * @param writable Whether the database must be opened in a writable mode or not. + * @returns returns 0 on success, or -1 on error. + */ + int open_db(std::string_view db_name, sqlite3 **db, const bool writable) + { + int ret; + const int flags = writable ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : SQLITE_OPEN_READONLY; + if ((ret = sqlite3_open_v2(db_name.data(), db, flags, 0)) != SQLITE_OK) + { + LOG_ERROR << ret << ": Sqlite error when opening database " << db_name; + *db = NULL; + return -1; + } + + return 0; + } + + /** + * Executes given sql query. + * @param db Pointer to the db. + * @param sql Sql query to be executed. + * @param callback Callback funcion which is called for each result row. + * @param callback_first_arg First data argumat to be parced to the callback (void pointer). + * @returns returns 0 on success, or -1 on error. + */ + int exec_sql(sqlite3 *db, std::string_view sql, int (*callback)(void *, int, char **, char **), void *callback_first_arg) + { + char *err_msg; + if (sqlite3_exec(db, sql.data(), callback, (callback != NULL ? (void *)callback_first_arg : NULL), &err_msg) != SQLITE_OK) + { + LOG_ERROR << "SQL error occured: " << err_msg; + sqlite3_free(err_msg); + return -1; + } + return 0; + } + + int begin_transaction(sqlite3 *db) + { + return sqlite::exec_sql(db, BEGIN_TRANSACTION); + } + + int commit_transaction(sqlite3 *db) + { + return sqlite::exec_sql(db, COMMIT_TRANSACTION); + } + + int rollback_transaction(sqlite3 *db) + { + return sqlite::exec_sql(db, ROLLBACK_TRANSACTION); + } + + /** + * Create a table with given table info. + * @param db Pointer to the db. + * @param table_name Table name to be created. + * @param column_info Column info of the table. + * @returns returns 0 on success, or -1 on error. + */ + int create_table(sqlite3 *db, std::string_view table_name, const std::vector &column_info) + { + std::string sql; + sql.append(CREATE_TABLE).append(table_name).append(" ("); + + for (auto itr = column_info.begin(); itr != column_info.end(); ++itr) + { + sql.append(itr->name); + sql.append(" "); + sql.append(COLUMN_DATA_TYPES[itr->column_type]); + + if (itr->is_key) + { + sql.append(" "); + sql.append(PRIMARY_KEY); + } + + if (!itr->is_null) + { + sql.append(" "); + sql.append(NOT_NULL); + } + + if (itr != column_info.end() - 1) + sql.append(","); + } + sql.append(")"); + + const int ret = exec_sql(db, sql); + if (ret == -1) + LOG_ERROR << "Error when creating sqlite table " << table_name; + + return ret; + } + + int create_index(sqlite3 *db, std::string_view table_name, std::string_view column_names, const bool is_unique) + { + std::string index_name = std::string("idx_").append(table_name).append("_").append(column_names); + std::replace(index_name.begin(), index_name.end(), ',', '_'); + + std::string sql; + sql.append(is_unique ? CREATE_UNIQUE_INDEX : CREATE_INDEX) + .append(index_name) + .append(" ON ") + .append(table_name) + .append("(") + .append(column_names) + .append(")"); + + const int ret = exec_sql(db, sql); + if (ret == -1) + LOG_ERROR << "Error when creating sqlite index '" << index_name << "' in table " << table_name; + + return ret; + } + + /** + * Inserts mulitple rows to a table. + * @param db Pointer to the db. + * @param table_name Table name to be populated. + * @param column_names_string Comma seperated string of colums (eg: "col_1,col_2,..."). + * @param value_strings Vector of comma seperated values (wrap in single quotes for TEXT type) (eg: ["r1val1,'r1val2',...", "r2val1,'r2val2',..."]). + * @returns returns 0 on success, or -1 on error. + */ + int insert_rows(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, const std::vector &value_strings) + { + std::string sql; + + sql.append(INSERT_INTO); + sql.append(table_name); + sql.append("("); + sql.append(column_names_string); + sql.append(") "); + sql.append(VALUES); + + for (auto itr = value_strings.begin(); itr != value_strings.end(); ++itr) + { + sql.append("("); + sql.append(*itr); + sql.append(")"); + + if (itr != value_strings.end() - 1) + sql.append(","); + } + + /* Execute SQL statement */ + return exec_sql(db, sql); + } + + /** + * Inserts a row to a table. + * @param db Pointer to the db. + * @param table_name Table name to be populated. + * @param column_names_string Comma seperated string of colums (eg: "col_1,col_2,..."). + * @param value_string comma seperated values as per column order (wrap in single quotes for TEXT type) (eg: "r1val1,'r1val2',..."). + * @returns returns 0 on success, or -1 on error. + */ + int insert_row(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, std::string_view value_string) + { + std::string sql; + // Reserving the space for the query before construction. + sql.reserve(sizeof(INSERT_INTO) + table_name.size() + column_names_string.size() + sizeof(VALUES) + value_string.size() + 5); + + sql.append(INSERT_INTO); + sql.append(table_name); + sql.append("("); + sql.append(column_names_string); + sql.append(") "); + sql.append(VALUES); + sql.append("("); + sql.append(value_string); + sql.append(")"); + + /* Execute SQL statement */ + return exec_sql(db, sql); + } + + /** + * Checks whether table exist in the database. + * @param db Pointer to the db. + * @param table_name Table name to be checked. + * @returns returns true is exist, otherwise false. + */ + bool is_table_exists(sqlite3 *db, std::string_view table_name) + { + std::string sql; + // Reserving the space for the query before construction. + sql.reserve(sizeof(SELECT_ALL) + sizeof(SQLITE_MASTER) + sizeof(WHERE) + sizeof(AND) + table_name.size() + 19); + + sql.append(SELECT_ALL); + sql.append(SQLITE_MASTER); + sql.append(WHERE); + sql.append("type='table'"); + sql.append(AND); + sql.append("name='"); + sql.append(table_name); + sql.append("'"); + + sqlite3_stmt *stmt; + + if (sqlite3_prepare_v2(db, sql.data(), -1, &stmt, 0) == SQLITE_OK && + stmt != NULL && sqlite3_step(stmt) == SQLITE_ROW) + { + // Finalize and distroys the statement. + sqlite3_finalize(stmt); + return true; + } + + // Finalize and distroys the statement. + sqlite3_finalize(stmt); + return false; + } + + /** + * Closes a connection to a given databse. + * @param db Pointer to the db. + * @returns returns 0 on success, or -1 on error. + */ + int close_db(sqlite3 **db) + { + if (*db == NULL) + return 0; + + if (sqlite3_close(*db) != SQLITE_OK) + { + LOG_ERROR << "Can't close database: " << sqlite3_errmsg(*db); + return -1; + } + + *db = NULL; + return 0; + } +} diff --git a/src/sqlite.hpp b/src/sqlite.hpp new file mode 100644 index 0000000..872afd7 --- /dev/null +++ b/src/sqlite.hpp @@ -0,0 +1,65 @@ +#ifndef _SA_SQLITE_ +#define _SA_SQLITE_ + +#include "pchheader.hpp" + +namespace sqlite +{ + /** + * Define an enum and a string array for the column data types. + * Any column data type that needs to be supportes should be added to both the 'COLUMN_DATA_TYPE' enum and the 'column_data_type' array in its respective order. + */ + enum COLUMN_DATA_TYPE + { + INT, + TEXT, + BLOB + }; + + /** + * Struct of table column information. + * { + * string name Name of the column. + * column_type Data type of the column. + * is_key Whether column is a key. + * is_null Whether column is nullable. + * } + */ + struct table_column_info + { + std::string name; + COLUMN_DATA_TYPE column_type; + bool is_key; + bool is_null; + + table_column_info(std::string_view name, const COLUMN_DATA_TYPE &column_type, const bool is_key = false, const bool is_null = true) + : name(name), column_type(column_type), is_key(is_key), is_null(is_null) + { + } + }; + + int open_db(std::string_view db_name, sqlite3 **db, const bool writable = false); + + int exec_sql(sqlite3 *db, std::string_view sql, int (*callback)(void *, int, char **, char **) = NULL, void *callback_first_arg = NULL); + + int begin_transaction(sqlite3 *db); + + int commit_transaction(sqlite3 *db); + + int rollback_transaction(sqlite3 *db); + + int create_table(sqlite3 *db, std::string_view table_name, const std::vector &column_info); + + int create_index(sqlite3 *db, std::string_view table_name, std::string_view column_names, const bool is_unique); + + int insert_rows(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, const std::vector &value_strings); + + int insert_row(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, std::string_view value_string); + + bool is_table_exists(sqlite3 *db, std::string_view table_name); + + int close_db(sqlite3 **db); + + +} +#endif diff --git a/src/util/util.cpp b/src/util/util.cpp new file mode 100644 index 0000000..f161724 --- /dev/null +++ b/src/util/util.cpp @@ -0,0 +1,100 @@ +#include "../pchheader.hpp" +#include "util.hpp" + +namespace util +{ + /** + * Check whether given directory exists. + * @param path Directory path. + * @return Returns true if given directory exists otherwise false. + */ + bool is_dir_exists(std::string_view path) + { + struct stat st; + return (stat(path.data(), &st) == 0 && S_ISDIR(st.st_mode)); + } + + /** + * Check whether given file exists. + * @param path File path. + * @return Returns true if give file exists otherwise false. + */ + bool is_file_exists(std::string_view path) + { + struct stat st; + return (stat(path.data(), &st) == 0 && S_ISREG(st.st_mode)); + } + + /** + * Recursively creates directories and sub-directories if not exist. + * @param path Directory path. + * @return Returns 0 operations succeeded otherwise -1. + */ + int create_dir_tree_recursive(std::string_view path) + { + if (strcmp(path.data(), "/") == 0) // No need of checking if we are at root. + return 0; + + // Check whether this dir exists or not. + struct stat st; + if (stat(path.data(), &st) != 0 || !S_ISDIR(st.st_mode)) + { + // Check and create parent dir tree first. + char *path2 = strdup(path.data()); + char *parent_dir_path = dirname(path2); + bool error_thrown = false; + + if (create_dir_tree_recursive(parent_dir_path) == -1) + error_thrown = true; + + free(path2); + + // Create this dir. + if (!error_thrown && mkdir(path.data(), S_IRWXU | S_IRWXG | S_IROTH) == -1) + { + std::cerr << errno << ": Error in recursive dir creation. " << path << std::endl; + error_thrown = true; + } + + if (error_thrown) + return -1; + } + + return 0; + } + + /** + * Reads the entire file from given file discriptor. + * @param fd File descriptor to be read. + * @param buf String buffer to be populated. + * @param offset Begin offset of the file to read. + * @return Returns number of bytes read in a successful read and -1 on error. + */ + int read_from_fd(const int fd, std::string &buf, const off_t offset) + { + struct stat st; + if (fstat(fd, &st) == -1) + { + std::cerr << errno << ": Error in stat for reading entire file." << std::endl; + return -1; + } + + buf.resize(st.st_size - offset); + + return pread(fd, buf.data(), buf.size(), offset); + } + + /** + * Provide a safe std::string overload for realpath. + * @param path Path. + * @returns Returns the realpath as string. + */ + const std::string realpath(std::string_view path) + { + std::array buffer; + ::realpath(path.data(), buffer.data()); + buffer[PATH_MAX] = '\0'; + return buffer.data(); + } + +} // namespace util diff --git a/src/util/util.hpp b/src/util/util.hpp new file mode 100644 index 0000000..a885ec7 --- /dev/null +++ b/src/util/util.hpp @@ -0,0 +1,23 @@ +#ifndef _HP_UTIL_UTIL_ +#define _HP_UTIL_UTIL_ + +#include "../pchheader.hpp" + +/** + * Contains helper functions and data structures used by multiple other subsystems. + */ +namespace util +{ + bool is_dir_exists(std::string_view path); + + bool is_file_exists(std::string_view path); + + int create_dir_tree_recursive(std::string_view path); + + int read_from_fd(const int fd, std::string &buf, const off_t offset = 0); + + const std::string realpath(std::string_view path); + +} // namespace util + +#endif