Compare commits

...

10 commits

32 changed files with 1862 additions and 2 deletions

7
.gitignore vendored
View file

@ -1,4 +1,10 @@
# ---> C++
# LSP Cache
.cache/
# Build dirs
build/
# Prerequisites
*.d
@ -31,4 +37,3 @@
*.exe
*.out
*.app

8
CMake/Catch2.cmake Normal file
View file

@ -0,0 +1,8 @@
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.0
)
FetchContent_MakeAvailable(Catch2)

28
CMake/sqlite3.cmake Normal file
View file

@ -0,0 +1,28 @@
include(FetchContent)
FetchContent_Declare(
sqlite3
URL https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(
sqlite3
)
add_library(
sqlite3
STATIC
)
target_include_directories(
sqlite3
PUBLIC
${sqlite3_SOURCE_DIR}
)
target_sources(
sqlite3
PRIVATE
${sqlite3_SOURCE_DIR}/sqlite3.c
)

11
CMakeLists.txt Normal file
View file

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.21)
project(Garbage
LANGUAGES C CXX
)
add_subdirectory(Garbage)
if(PROJECT_IS_TOP_LEVEL)
add_subdirectory(Tests)
endif()

11
Garbage/CMakeLists.txt Normal file
View file

@ -0,0 +1,11 @@
add_library(Garbage INTERFACE)
target_compile_features(Garbage
INTERFACE
cxx_std_20
)
target_include_directories(Garbage
INTERFACE
"Include"
)

View file

@ -0,0 +1,170 @@
#ifndef GARBAGE_BASE64_HPP
#define GARBAGE_BASE64_HPP
#include <array>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace Garbage::Base64
{
namespace Internal
{
struct SmallBuffer
{
public:
[[nodiscard]] constexpr
SmallBuffer()
: mStoredBits(0), mData(0)
{}
constexpr
void Enqueue(std::uint8_t byte, std::size_t bits = 8)
{
std::uint8_t mask = (1u << bits) - 1;
mData = (mData << bits) | (byte & mask);
mStoredBits += bits;
}
[[nodiscard]] constexpr
std::uint8_t Dequeue(std::size_t bits = 8)
{
std::uint64_t mask = (1u << bits) - 1;
mStoredBits -= bits;
std::uint64_t out = (mData >> mStoredBits) & mask;
return out;
}
[[nodiscard]] constexpr
std::size_t Size()
{
return mStoredBits;
}
[[nodiscard]] constexpr
std::size_t Capacity()
{
return 64;
}
private:
std::size_t mStoredBits;
std::uint64_t mData;
};
static constexpr auto EncodeLUT = std::array {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/',
};
static constexpr std::size_t DecodeOffset = '+';
static constexpr auto DecodeLUT = std::array {
0b111'110, /* + */ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/
0b111'111, /* / */ 0b110'100, /* 0 */ 0b110'101, /* 1 */ 0b110'110, /* 2 */
0b110'111, /* 3 */ 0b111'000, /* 4 */ 0b111'001, /* 5 */ 0b111'010, /* 6 */
0b111'011, /* 7 */ 0b111'100, /* 8 */ 0b111'101, /* 9 */ 0b000'000, /*---*/
0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/
0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /* A */ 0b000'001, /* B */
0b000'010, /* C */ 0b000'011, /* D */ 0b000'100, /* E */ 0b000'101, /* F */
0b000'110, /* G */ 0b000'111, /* H */ 0b001'000, /* I */ 0b001'001, /* J */
0b001'010, /* K */ 0b001'011, /* L */ 0b001'100, /* M */ 0b001'101, /* N */
0b001'110, /* O */ 0b001'111, /* P */ 0b010'000, /* Q */ 0b010'001, /* R */
0b010'010, /* S */ 0b010'011, /* T */ 0b010'100, /* U */ 0b010'101, /* V */
0b010'110, /* W */ 0b010'111, /* X */ 0b011'000, /* Y */ 0b011'001, /* Z */
0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/
0b000'000, /*---*/ 0b000'000, /*---*/ 0b011'010, /* a */ 0b011'011, /* b */
0b011'100, /* c */ 0b011'101, /* d */ 0b011'110, /* e */ 0b011'111, /* f */
0b100'000, /* g */ 0b100'001, /* h */ 0b100'010, /* i */ 0b100'011, /* j */
0b100'100, /* k */ 0b100'101, /* l */ 0b100'110, /* m */ 0b100'111, /* n */
0b101'000, /* o */ 0b101'001, /* p */ 0b101'010, /* q */ 0b101'011, /* r */
0b101'100, /* s */ 0b101'101, /* t */ 0b101'110, /* u */ 0b101'111, /* v */
0b110'000, /* w */ 0b110'001, /* x */ 0b110'010, /* y */ 0b110'011, /* z */
};
}
[[nodiscard]] constexpr
std::string Encode(std::span<std::uint8_t> data) {
std::string result;
result.reserve(data.size() + data.size() / 2);
Internal::SmallBuffer buffer;
std::size_t i = 0;
while (i < data.size()) {
while (buffer.Capacity() - buffer.Size() >= 8 && i < data.size()) {
buffer.Enqueue(data[i++]);
}
while (buffer.Size() >= 6) {
result.push_back(Internal::EncodeLUT[buffer.Dequeue(6)]);
}
}
if (buffer.Size() > 0) {
std::size_t remainingBits = buffer.Size();
std::uint8_t data = (buffer.Dequeue(remainingBits) << (6 - remainingBits));
result.push_back(Internal::EncodeLUT[data]);
}
if (data.size() % 3) {
result.append(std::string(4 - (result.size() % 4), '='));
}
return result;
}
[[nodiscard]] constexpr
std::vector<std::uint8_t> Decode(std::string_view data) {
std::vector<std::uint8_t> result;
result.reserve(data.size());
std::size_t end = data.size();
end -= (data.size() > 0 && data[data.size() - 1] == '=');
end -= (data.size() > 1 && data[data.size() - 2] == '=');
Internal::SmallBuffer buffer;
std::size_t i = 0;
while (i < end) {
while (buffer.Capacity() - buffer.Size() >= 6 && i < end) {
buffer.Enqueue(Internal::DecodeLUT[data[i++] - Internal::DecodeOffset], 6);
}
while (buffer.Size() >= 8) {
result.push_back(buffer.Dequeue(8));
}
}
return result;
}
[[nodiscard]] constexpr
bool Validate(std::string_view data)
{
// Technically superfluous - the decoder can deal with unpadded data.
if (data.size() % 4 != 0) {
return false;
}
std::size_t i = 0;
for (char ch : data) {
bool condition = (ch == '+') ||
(ch >= '/' && ch <= '9') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z');
if (!condition) {
// Allow '=' for the last two character
if (i >= data.size() - 2 && ch == '=') {
continue;
}
return false;
}
++i;
}
return true;
}
}
#endif // GARBAGE_BASE64_HPP

View file

@ -0,0 +1,742 @@
#ifndef GARBAGE_SQLITE_HPP
#define GARBAGE_SQLITE_HPP
#include <array>
#include <cstdint>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <tuple>
#include <vector>
#include <sqlite3.h>
namespace Garbage::SQLite
{
//////////////////////////////////////////////////////////////////////////
// TRAIT SUPPORT
template <typename T>
struct MemberTraits;
template <typename T, typename U>
struct MemberTraits<T U::*>
{
using Member = T;
using Container = U;
};
namespace Implementation
{
template <typename... Ts>
struct First;
template <typename T, typename... Ts>
struct First<T, Ts...>
{
using Type = T;
};
}
template <typename... Ts>
using First = Implementation::First<Ts...>::Type;
namespace Implementation
{
template <template <typename> typename Template, typename T>
struct IsSpecialization
{
static constexpr bool Value = false;
};
template <template <typename> typename Template, typename... Args>
struct IsSpecialization<Template, Template<Args...>>
{
static constexpr bool Value = true;
};
template <template <auto> typename Template, typename T>
struct IsValueSpecialization
{
static constexpr bool Value = false;
};
template <template <auto> typename Template, auto... Args>
struct IsValueSpecialization<Template, Template<Args...>>
{
static constexpr bool Value = true;
};
}
template <template <typename> typename Template, typename T>
concept IsSpecialization = Implementation::IsSpecialization<Template, T>::Value;
template <template <auto> typename Template, typename T>
concept IsValueSpecialization = Implementation::IsValueSpecialization<Template, T>::Value;
template <typename T1, typename T2>
concept IsSame = std::is_same_v<T1, T2>;
template <typename T, typename... Ts>
concept IsAnyOf = (IsSame<T, Ts> || ...);
template <template <typename> typename Wrapper, typename T, typename... Ts>
concept IsAnyInstanceOf = IsAnyOf<T, Wrapper<Ts>...>;
namespace Implementation
{
template <typename Arg, typename... Ts>
struct FirstWithArg
{
using Type = void;
};
template <typename Arg, template <typename> typename T, typename... Ts, typename... Args>
struct FirstWithArg<Arg, T<Args...>, Ts...>
{
using Type = std::conditional_t<IsSame<Arg, typename First<Args...>::Type>,
T<Args...>, typename FirstWithArg<Arg, Ts...>::Type>;
};
}
template <typename Arg, typename... Ts>
using FirstWithArg = Implementation::FirstWithArg<Arg, Ts...>::Type;
//////////////////////////////////////////////////////////////////////////
// UTILITY FUNCTIONS
template <typename DestinationType, typename... TupleTypes>
[[nodiscard]] constexpr
auto ToArray(std::tuple<TupleTypes...>&& tuple)
{
constexpr auto makeArray = [] (auto&&... args) {
return std::array{ std::forward(args)... };
};
return std::apply(makeArray, tuple);
}
template <typename T, std::size_t N>
[[nodiscard]] constexpr
auto ToArray(auto (&&array)[N])
{
std::array<T, N> result;
std::move(std::begin(array), std::end(array), result.begin());
return result;
}
// Note(3011): The mutating version is easily addable if need be.
template <typename... Ts>
constexpr
void ForEach(auto&& multifunc, const std::tuple<Ts...>& tuple)
{
auto conditionalInvoke = [] (auto& multifunc, const auto& item) {
if constexpr (std::invocable<decltype(multifunc), decltype(item)>) {
multifunc(item);
}
};
std::apply([&multifunc, &conditionalInvoke] (const auto&... args) {
(conditionalInvoke(multifunc, args), ...);
}, tuple);
}
//////////////////////////////////////////////////////////////////////////
// ERRORS
class DatabaseError : public std::runtime_error
{
using std::runtime_error::runtime_error;
};
//////////////////////////////////////////////////////////////////////////
// BASIC TYPES
using Integer = std::int64_t;
using String = std::string;
using Real = double;
using Blob = std::vector<std::uint8_t>;
template <typename T>
using Optional = std::optional<T>;
namespace Implementation
{
template <typename T>
struct TypeString;
template <> struct TypeString<Integer> { static constexpr std::string_view Value = "INTEGER"; };
template <> struct TypeString<String > { static constexpr std::string_view Value = "TEXT" ; };
template <> struct TypeString<Real > { static constexpr std::string_view Value = "REAL" ; };
template <> struct TypeString<Blob > { static constexpr std::string_view Value = "BLOB" ; };
template <typename T>
struct TypeString<Optional<T>>
{
static constexpr std::string_view Value = TypeString<T>::Value;
};
}
template <typename T>
static constexpr std::string_view TypeString = Implementation::TypeString<T>::Value;
template <typename T>
concept SupportedType = IsAnyOf<T, Integer, String, Real, Blob>
|| IsAnyInstanceOf<Optional, T, Integer, String, Real, Blob>;
// TODO: These are only for development, remove once prepared statements are
// implemented.
[[nodiscard]] constexpr
std::string ToString(Integer i)
{
return std::to_string(i);
}
[[nodiscard]] constexpr
std::string ToString(const String& s)
{
return "'" + s + "'";
}
//////////////////////////////////////////////////////////////////////////
// DATABASE MODEL
enum class ColumnFlags : std::uint8_t
{
Nothing = 0b0000'0000,
PrimaryKey = 0b0000'0001,
Unique = 0b0000'0010,
LowerBits = 0b0000'1111,
UpperBits = 0b1111'0000,
};
[[nodiscard]] constexpr
ColumnFlags operator| (ColumnFlags a, ColumnFlags b)
{
using Type = std::underlying_type_t<ColumnFlags>;
return ColumnFlags(static_cast<Type>(a) | static_cast<Type>(b));
}
[[nodiscard]] constexpr
ColumnFlags operator& (ColumnFlags a, ColumnFlags b)
{
using Type = std::underlying_type_t<ColumnFlags>;
return ColumnFlags(static_cast<Type>(a) & static_cast<Type>(b));
}
template <ColumnFlags Flag>
[[nodiscard]] constexpr
bool IsSet(ColumnFlags a)
{
return (a & Flag) != ColumnFlags::Nothing;
}
template <typename T>
requires std::is_member_object_pointer_v<T>
&& SupportedType<typename MemberTraits<T>::Member>
class Column
{
public:
using PtrType = T;
using DataType = MemberTraits<T>::Member;
using TableType = MemberTraits<T>::Container;
[[nodiscard]] constexpr
Column(DataType TableType::*ptr, std::string_view name, ColumnFlags flags = ColumnFlags::Nothing)
: mPtr(ptr), mFlags(flags), mName(name)
{}
std::string_view Name() const
{
return mName;
}
std::string_view Type() const
{
return TypeString<DataType>;
}
ColumnFlags Flags() const
{
return mFlags;
}
bool IsOptional() const
{
// Note: it doesn't work with the Optional alias
return IsSpecialization<std::optional, DataType>;
}
std::string Declaration() const
{
std::string result;
result += mName + " " + std::string(Type());
if (!IsOptional()) {
result += " NOT NULL";
}
switch (mFlags & ColumnFlags::LowerBits) {
break; case ColumnFlags::Nothing:
// noop
break; case ColumnFlags::PrimaryKey:
result += " PRIMARY KEY";
break; case ColumnFlags::Unique:
result += " UNIQUE";
break; default:
throw DatabaseError("Invalid column flag for: " + mName);
}
return result;
}
DataType TableType::*Ptr() const
{
return mPtr;
}
const DataType& Get(const TableType& item) const
{
return item.*mPtr;
}
private:
DataType TableType::*mPtr;
ColumnFlags mFlags;
std::string mName;
};
template <typename T>
Column(T, auto) -> Column<T>;
template <typename T>
Column(T, auto, ColumnFlags) -> Column<T>;
template <std::size_t N>
class ForeignKey
{
public:
template <typename StringLike>
requires std::is_convertible_v<StringLike, std::string>
[[nodiscard]] constexpr
ForeignKey(StringLike (&&from)[N], std::string_view table, StringLike (&&to)[N])
: mDestinationTable(table)
, mFromColumns(ToArray<std::string>(std::move(from)))
, mToColumns(ToArray<std::string>(std::move(to)))
{}
std::string Declaration() const
{
std::string fromColumns;
std::string toColumns;
for (std::size_t i = 0; i < mFromColumns.size(); ++i) {
fromColumns += mFromColumns[i];
toColumns += mToColumns[i];
if (i < N - 1) {
fromColumns += ", ";
toColumns += ", ";
}
}
return "FOREIGN KEY(" + fromColumns + ") REFERENCES " + mDestinationTable + "(" + toColumns + ")";
}
private:
std::string mDestinationTable;
std::array<std::string, N> mFromColumns;
std::array<std::string, N> mToColumns;
};
template <typename T>
concept MappableType = requires {
requires std::is_aggregate_v<T>;
requires std::is_default_constructible_v<T>;
requires !std::is_array_v<T>;
};
template <typename T, typename TableType>
concept TableEntity = IsValueSpecialization<ForeignKey, T>
|| (IsSpecialization<Column, T> && IsSame<TableType, typename T::TableType>);
template <MappableType T, TableEntity<T>... Entities>
requires (sizeof...(Entities) > 0)
class Table
{
public:
[[nodiscard]] constexpr
Table(std::string_view name, Entities&&... entities)
: mName(name), mEntities(std::move(entities)...)
{}
std::string_view Name() const
{
return mName;
}
template <typename ColumnPtr>
std::string_view Name(ColumnPtr ptr) const
{
std::string_view name;
ForEach([&name, &ptr] (const Column<ColumnPtr>& column) {
if (ptr == column.Ptr()) {
name = column.Name();
}
}, mEntities);
return name;
}
template <typename ColumnPtr>
std::string Identifier(ColumnPtr ptr) const
{
return "$" + mName + "::" + std::string(Name(ptr));
}
std::string Create() const
{
std::string result;
result += "CREATE TABLE IF NOT EXISTS " + mName + " (\n";
ForEach([&result] <typename ColumnType> (const Column<ColumnType>& column) {
result += " ";
result += column.Declaration();
result += ",\n";
}, mEntities);
ForEach([&result] <std::size_t Size> (const ForeignKey<Size>& key) {
result += " ";
result += key.Declaration();
result += ",\n";
}, mEntities);
result.erase(result.size() - 2, 1);
result.back() = '\n';
result += ");\n";
return result;
}
std::string Insert() const
{
std::string columnNames;
std::string valueNames;
ForEach([this, &columnNames, &valueNames] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
columnNames += column.Name();
columnNames += ", ";
valueNames += Identifier(column.Ptr());
valueNames += ", ";
}, mEntities);
std::string result;
result += "INSERT INTO " + mName + "(";
result += columnNames.erase(columnNames.size() - 2);
result += ") VALUES (";
result += valueNames.erase(valueNames.size() - 2);
result += ");\n";
return result;
}
// TODO: These are only for development, remove once prepared statements are
// implemented.
std::string Insert(const T& item) const
{
std::string columnNames;
std::string values;
ForEach([&item, &columnNames, &values] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
columnNames += column.Name();
columnNames += ", ";
values += ToString(column.Get(item));
values += ", ";
}, mEntities);
std::string result;
result += "INSERT INTO " + mName + "(";
result += columnNames.erase(columnNames.size() - 2);
result += ") VALUES (";
result += values.erase(values.size() - 2);
result += ");\n";
return result;
}
std::string Update() const
{
std::string result;
result += "UPDATE " + mName + " SET\n";
ForEach([this, &result] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
result += " " + std::string(column.Name()) + " = " + Identifier(column.Ptr()) + ",\n";
}, mEntities);
result.erase(result.size() - 2, 1);
ForEach([this, &result] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
result += "WHERE " + std::string(column.Name()) + " = " + Identifier(column.Ptr()) + ";\n";
}
}, mEntities);
return result;
}
// TODO: These are only for development, remove once prepared statements are
// implemented.
std::string Update(const T& item) const
{
std::string result;
result += "UPDATE " + mName + " SET\n";
ForEach([this, &result, &item] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
result += " " + std::string(column.Name()) + " = " + ToString(column.Get(item)) + ",\n";
}, mEntities);
result.erase(result.size() - 2, 1);
ForEach([this, &result, &item] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
result += "WHERE " + std::string(column.Name()) + " = " + ToString(column.Get(item)) + ";\n";
}
}, mEntities);
return result;
}
private:
std::string mName;
std::tuple<Entities...> mEntities;
};
// Note(3011): This breaks when the first specified entity is not a column.
template <typename... Entities>
Table(auto, Entities&&...) -> Table<typename MemberTraits<typename First<Entities...>::PtrType>::Container, Entities...>;
template <typename... Tables>
requires (IsSpecialization<Table, Tables> && ...)
class Database
{
public:
[[nodiscard]] constexpr
Database(std::string_view name, Tables&&... tables)
: mName(name), mTables(std::move(tables)...)
{}
template <typename TableType>
std::string_view Name() const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Name();
}
template <typename ColumnPtr>
requires std::is_member_object_pointer_v<ColumnPtr>
std::string_view Name(ColumnPtr ptr) const
{
using TableType = MemberTraits<ColumnPtr>::Container;
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Name(ptr);
}
template <typename ColumnPtr>
requires std::is_member_object_pointer_v<ColumnPtr>
std::string Identifier(ColumnPtr ptr) const
{
using TableType = MemberTraits<ColumnPtr>::Container;
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Identifier(ptr);
}
std::string Create() const
{
std::string result;
result += "PRAGMA foreign_keys = ON;\n\n";
std::apply([&result](auto&... args) {
((result += args.Create(), result += "\n"), ...);
}, mTables);
return result;
}
template <typename TableType>
std::string Insert() const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Insert();
}
template <typename TableType>
std::string Insert(const TableType& item) const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Insert(item);
}
template <typename TableType>
std::string Update() const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Update();
}
template <typename TableType>
std::string Update(const TableType& item) const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Update(item);
}
private:
std::string mName;
std::tuple<Tables...> mTables;
};
template <typename... Tables>
Database(auto, Tables...) -> Database<Tables...>;
//////////////////////////////////////////////////////////////////////////
// DATABASE CONNECTION
enum class Operator : std::uint8_t
{
None = 0,
Equal = 1,
};
template <typename DatabaseType>
requires IsSpecialization<Database, DatabaseType>
class Connection;
template <typename DatabaseType, typename TableType>
class Select
{
private:
friend class Connection<DatabaseType>;
Select(Connection<DatabaseType>& connection)
: mConnection(connection)
{}
public:
// Note: Immovable object, please don't use unstoppable force.
Select(const Select&) = delete;
Select& operator=(const Select&) = delete;
Select(Select&&) = delete;
Select& operator=(Select&&) = delete;
template <typename ColumnPtr>
requires std::is_member_object_pointer_v<ColumnPtr>
Select& Where(ColumnPtr column)
{
// TODO: Append the condition.
return &this;
}
// TODO: Finalizer member function - returns the result.
std::optional<TableType> First()
{
}
// TODO: Finalizer member function - returns the result.
std::vector<TableType> All()
{
}
private:
Connection<TableType>& mConnection;
std::string mStatement;
};
template <typename DatabaseType>
requires IsSpecialization<Database, DatabaseType>
class Connection
{
public:
Connection(const DatabaseType& database, std::string path)
: mDatabase(&database), mPath(std::move(path)), mConnection(nullptr)
{
int errc = sqlite3_open(mPath.c_str(), &mConnection);
if (errc != SQLITE_OK) {
// TODO: Add more information to the error.
throw DatabaseError("Could not open a SQLite connection");
}
}
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
Connection(Connection&& other) noexcept
: mDatabase(other.mDatabase)
, mPath(std::move(other.mPath))
, mConnection(other.mConnection)
{
other.mConnection = nullptr;
}
Connection& operator= (Connection&& other)
{
if (&other == this) {
return;
}
mDatabase = other.mDatabase;
mPath = std::move(other.mPath);
mConnection = other.mConnection;
other.mDatabase = nullptr;
other.mConnection = nullptr;
}
~Connection()
{
// TODO: The current implementation will loop here forever in case
// anything isn't finalized - make sure to force a cleanup before
// that happens.
int errc = sqlite3_close(mConnection);
while (errc == SQLITE_BUSY) {
errc = sqlite3_close(mConnection);
}
}
bool Create()
{
std::string script = mDatabase->Create();
char *errmsg;
auto errc = sqlite3_exec(mConnection, script.c_str(), nullptr, nullptr, &errmsg);
if (errc != SQLITE_OK) {
// TODO: Report the error?
sqlite3_free(reinterpret_cast<void *>(errc));
return false;
}
return true;
}
template <typename TableType>
bool Insert(TableType& item)
{
std::string statement = mDatabase->Insert(item);
char *errmsg;
auto errc = sqlite3_exec(mConnection, statement.c_str(), nullptr, nullptr, &errmsg);
if (errc != SQLITE_OK) {
// TODO: Report the error?
sqlite3_free(reinterpret_cast<void *>(errc));
return false;
}
return true;
}
template <typename TableType>
bool Update(TableType& item)
{
std::string statement = mDatabase->Insert(item);
char *errmsg;
auto errc = sqlite3_exec(mConnection, statement.c_str(), nullptr, nullptr, &errmsg);
if (errc != SQLITE_OK) {
// TODO: Report the error?
sqlite3_free(reinterpret_cast<void *>(errc));
return false;
}
return true;
}
template <typename TableType>
Select<DatabaseType, TableType> Select()
{
return Select<DatabaseType, TableType>(*this);
}
private:
const DatabaseType *mDatabase;
std::string mPath;
sqlite3 *mConnection;
};
}
#endif // GARBAGE_SQLITE_HPP

View file

@ -0,0 +1,314 @@
#ifndef GARBAGE_SIMPLE_CONF_HPP
#define GARBAGE_SIMPLE_CONF_HPP
#include <cctype>
#include <filesystem>
#include <format>
#include <fstream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
namespace Garbage
{
enum class SimpleConfErrorKind
{
ReadError,
ConversionError,
ParseError,
LookupError,
};
constexpr
std::string AsString(SimpleConfErrorKind err)
{
switch (err) {
case SimpleConfErrorKind::ReadError:
return "Read Error";
case SimpleConfErrorKind::ConversionError:
return "Conversion Error";
case SimpleConfErrorKind::ParseError:
return "Parse Error";
case SimpleConfErrorKind::LookupError:
return "Lookup Error";
default:
return "Unknown Error";
}
}
class SimpleConfError : public std::runtime_error
{
public:
SimpleConfError(SimpleConfErrorKind kind, std::string_view message)
: std::runtime_error(AsString(kind) + ": " + std::string(message))
{}
};
namespace SimpleConfImplementation
{
class ReadError : public SimpleConfError
{
public:
ReadError(std::string_view path, std::string_view message)
: SimpleConfError(SimpleConfErrorKind::ReadError,
std::format("[Path {}] {}", path, message))
{}
};
class ConversionError : public SimpleConfError
{
public:
ConversionError(std::string_view key, std::string_view message)
: SimpleConfError(SimpleConfErrorKind::ConversionError,
std::format("[Key {}] {}", key, message))
{}
};
class ParseError : public SimpleConfError
{
public:
ParseError(std::size_t line, std::string_view message)
: SimpleConfError(SimpleConfErrorKind::ParseError,
std::format("[Line {}] {}", line, message))
{}
};
class LookupError : public SimpleConfError
{
public:
LookupError(std::string_view message)
: SimpleConfError(SimpleConfErrorKind::LookupError, message)
{}
};
template <typename T>
constexpr
T Convert(std::string_view key, std::string_view rawValue)
{
if constexpr (std::is_same_v<T, std::string>) {
return std::string(rawValue);
}
if constexpr (std::is_integral_v<T> || std::is_floating_point_v<T>) {
T result{};
auto [ptr, ec] = std::from_chars(rawValue.data(),
rawValue.data() + rawValue.size(),
result);
if (ptr != rawValue.data() + rawValue.size()) {
throw ConversionError(key, "Could not parse the whole value");
}
else if (ec == std::errc::invalid_argument) {
throw ConversionError(key, "The value is not a valid number");
}
else if (ec == std::errc::result_out_of_range) {
throw ConversionError(key, "The value is out of range of the requested type");
}
else if (ec != std::errc()) {
throw ConversionError(key, "Unknown error");
}
return result;
}
throw ConversionError(key, "Can't convert the raw value to the requested type");
}
using PairsType = std::unordered_map<std::string_view, std::string_view>;
constexpr
void Parse(const std::string& rawConf, PairsType& pairs)
{
std::size_t current = 0;
std::size_t currentLine = 1;
enum class State
{
Initial,
ReadingKey,
ReadingValue,
CommentLine,
} currentState = State::Initial;
std::size_t keyBegin = 0;
std::size_t keyEnd = 0;
std::size_t valueBegin = 0;
std::size_t valueEnd = 0;
auto addPair = [&] {
if (keyBegin == keyEnd) {
throw ParseError(currentLine, "Empty key is not allowed");
}
std::string_view key(rawConf.data() + keyBegin, keyEnd - keyBegin);
std::string_view value(rawConf.data() + valueBegin, valueEnd - valueBegin);
if (value.size() == 0) {
value = std::string_view(nullptr, 0);
}
pairs[key] = value;
keyBegin = 0;
keyEnd = 0;
valueBegin = 0;
valueEnd = 0;
};
while (current < rawConf.size()) {
switch (currentState) {
break; case State::Initial:
if (std::isspace(rawConf[current])) {
if (rawConf[current] == '\n') {
++currentLine;
}
++current;
}
else if (rawConf[current] == '#') {
currentState = State::CommentLine;
}
else if (rawConf[current] == '=') {
throw ParseError(currentLine, "Empty key is not allowed");
}
else {
keyBegin = current;
keyEnd = current + 1; // We know there is at least one char
currentState = State::ReadingKey;
}
break; case State::ReadingKey:
if (rawConf[current] == '=') {
currentState = State::ReadingValue;
}
else if (rawConf[current] == '#') {
throw ParseError(currentLine, "Unexpected comment start");
}
else if (!std::isspace(rawConf[current])) {
keyEnd = current + 1;
}
else if (rawConf[current] == '\n') {
throw ParseError(currentLine, "End of line while reading key");
}
++current;
break; case State::ReadingValue:
if (rawConf[current] == '\n') {
addPair();
++currentLine;
currentState = State::Initial;
}
else if (rawConf[current] == '=') {
throw ParseError(currentLine, "Multiple key/value separators are illegal");
}
else if (rawConf[current] == '#') {
addPair();
currentState = State::CommentLine;
}
else {
if (!std::isspace(rawConf[current])) {
if (valueBegin == 0) {
valueBegin = current;
valueEnd = current + 1;
}
else {
valueEnd = current + 1;
}
}
}
++current;
break; case State::CommentLine:
if (rawConf[current] == '\n') {
++currentLine;
currentState = State::Initial;
}
++current;
break; default:
throw ParseError(currentLine, "Invalid parser state (should be impossible)");
}
}
if (currentState == State::ReadingKey) {
throw ParseError(currentLine, "End of config while reading the key");
} else if (currentState == State::ReadingValue) {
addPair();
}
}
}
class SimpleConf
{
public:
struct Required {};
SimpleConf(const std::filesystem::path& path)
{
namespace fs = std::filesystem;
using namespace SimpleConfImplementation;
std::error_code ec;
if (!fs::exists(path, ec) || ec) {
throw ReadError(path.string(), "The specified file does not exist");
}
if (!fs::is_regular_file(path, ec) || ec) {
throw ReadError(path.string(), "The the specified path is not a regular file");
}
std::size_t fileSize = fs::file_size(path, ec);
if (ec) {
throw ReadError(path.string(), "Could not read the file size");
}
std::ifstream file(path, std::ios::binary);
if (!file) {
throw ReadError(path.string(), "Could not open the file for reading");
}
mConfContent = std::string(fileSize, '\0');
file.read(mConfContent.data(), fileSize);
Parse(mConfContent, mPairs);
}
SimpleConf(std::string&& strConfig)
: mConfContent(std::move(strConfig))
{
using namespace SimpleConfImplementation;
Parse(mConfContent, mPairs);
}
template <typename T>
std::optional<T> GetOptional(std::string_view key)
{
using namespace SimpleConfImplementation;
if (auto it = mPairs.find(key); it != mPairs.end()) {
return Convert<T>(key, it->second);
}
return std::nullopt;
}
template <typename T, typename RequireValue = void>
T Get(std::string_view key, const T& defaultValue = {})
{
using namespace SimpleConfImplementation;
if (auto it = mPairs.find(key); it != mPairs.end()) {
return Convert<T>(key, it->second);
}
if constexpr (std::is_same_v<RequireValue, Required>) {
throw LookupError(std::format("Required config key \"{}\" not found", key));
}
return defaultValue;
}
private:
std::string mConfContent;
SimpleConfImplementation::PairsType mPairs;
};
}
#endif // GARBAGE_SIMPLE_CONF_HPP

107
README.md
View file

@ -1,3 +1,108 @@
# Garbage
A collection of various single-header libraries of questionable quality.
A collection of various single-header libraries of questionable quality.
## Usage
To use these libraries (please don't), just add them into your project's include path.
Ideally, use CMake with FetchContent pinned to a specific commit hash so you don't encounter
nasty surprises when the libraries update.
```cmake
include(FetchContent)
FetchContent_Declare(
Garbage
GIT_REPOSITORY https://code.3011.io/TennesseeTrash/Garbage
GIT_TAG 72f02ff856ddfd1b817d4f95096ec1be0ed11a49
)
FetchContent_MakeAvailable(
Garbage
)
```
Once you've added the `Include` directory into your include path, you can use the headers you need,
e.g.
```cpp
#include <Garbage/Base64.hpp>
// Now you can use
Garbage::Base64::Decode(/* some base64 string */);
```
## Base64
A very simple library composed of 3 functions in the `Garbage::Base64` namespace.
- `Encode()` takes a `std::span<std::uint8_t>` (i.e. an array of bytes), and produces a
padded Base64 encoded string out of it.
- `Decode()` takes an encoded `std::string_view`, and converts it into an array of bytes
(specifically a `std::vector<std::uint8_t>`).
- `Validate()` performs validation of an untrusted `std::string_view`. This is necessary
because `Decode()` does not do any checking and running it on unverified data is
dangerous. This function is not completely robust, but it is good enough to make sure
the `Decode()` call is safe.
## SimpleConf
A small config file library. Provides a `Garbage::SimpleConf` class that reads files
with key-value pairs. This library makes use of exceptions, it will throw `Garbage::SimpleConfError`
when an error is encountered. These exceptions inerit from `std::exception`, so using those to catch
them works fine.
```cpp
///////////////////////////////////////////////////////////////////////////////
// INIT
// Read a file from disk
Garbage::SimpleConf config(std::filesystem::path(/*path to the config*/));
// Use an externally provided string
std::string rawConfig = /* whatever procedure to obtain a string */;
Garbage::SimpleConf config(std::move(rawConfig));
///////////////////////////////////////////////////////////////////////////////
// GETTING VALUES
// Optional values
// Get a std::optional
auto value = config.GetOptional<std::string>("SomeKey");
// Get a default value
int value = config.Get<int>("SomeKey", 0 /* Optional default value */);
// Required values
using Required = Garbage::SimpleConf::Required;
int value = config.Get<int, Required>("SomeKey");
```
The config files are structured such that a single line contains a key-value pair separated by a `=`
character.
Some specifics:
- Leading and trailing whitespace is removed from both keys and values.
- Whitespace is is allowed inside both keys and values.
- Values may be empty.
- The `#` character denotes the start of a comment, everything after (and including)
this character is ignored until the end of the line.
Example:
```conf
# Simplest scenario
SomeKey=SomeValue
# Also works
SomeKey = SomeValue
# Also supported, but note that the key is now "Some Key"
Some Key = Some Value
Comments = After # The pair are also fine
# Also fine
KeyOnly=
```
## SQLite
WIP

View file

@ -0,0 +1,17 @@
add_executable(TestBase64)
target_compile_features(TestBase64
PRIVATE
cxx_std_20
)
target_link_libraries(TestBase64
PRIVATE
Garbage
Catch2::Catch2WithMain
)
target_sources(TestBase64
PRIVATE
Tests.cpp
)

81
Tests/Base64/Tests.cpp Normal file
View file

@ -0,0 +1,81 @@
#include <catch2/catch_test_macros.hpp>
#include <Garbage/Base64.hpp>
TEST_CASE("Base64 encoding/decoding tests")
{
SECTION("Empty span")
{
std::vector<std::uint8_t> data;
std::string encoded = Garbage::Base64::Encode(data);
REQUIRE(encoded.size() == 0);
REQUIRE(Garbage::Base64::Validate(encoded));
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(encoded);
REQUIRE(encoded.size() == 0);
}
SECTION("Encoding short data")
{
std::string source("Many hands make light work.");
std::vector<std::uint8_t> data(source.begin(), source.end());
std::string encoded = Garbage::Base64::Encode(data);
REQUIRE(encoded == "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu");
REQUIRE(Garbage::Base64::Validate(encoded));
}
SECTION("Decoding short data")
{
std::string data("TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu");
REQUIRE(Garbage::Base64::Validate(data));
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(data);
std::string destination(decoded.begin(), decoded.end());
REQUIRE(destination == "Many hands make light work.");
}
SECTION("Encoding with a padding char")
{
std::string source("With a padding");
std::vector<std::uint8_t> data(source.begin(), source.end());
std::string encoded = Garbage::Base64::Encode(data);
REQUIRE(encoded == "V2l0aCBhIHBhZGRpbmc=");
REQUIRE(Garbage::Base64::Validate(encoded));
}
SECTION("Decoding with a padding char")
{
std::string data("V2l0aCBhIHBhZGRpbmc=");
REQUIRE(Garbage::Base64::Validate(data));
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(data);
std::string destination(decoded.begin(), decoded.end());
REQUIRE(destination == "With a padding");
}
SECTION("Encoding with two padding chars")
{
std::string source("With two padding");
std::vector<std::uint8_t> data(source.begin(), source.end());
std::string encoded = Garbage::Base64::Encode(data);
REQUIRE(encoded == "V2l0aCB0d28gcGFkZGluZw==");
REQUIRE(Garbage::Base64::Validate(encoded));
}
SECTION("Decoding with two padding chars")
{
std::string data("V2l0aCB0d28gcGFkZGluZw==");
REQUIRE(Garbage::Base64::Validate(data));
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(data);
std::string destination(decoded.begin(), decoded.end());
REQUIRE(destination == "With two padding");
}
}

5
Tests/CMakeLists.txt Normal file
View file

@ -0,0 +1,5 @@
include(${PROJECT_SOURCE_DIR}/CMake/Catch2.cmake)
add_subdirectory(Base64)
add_subdirectory(SQLite)
add_subdirectory(SimpleConf)

View file

@ -0,0 +1,20 @@
include(${PROJECT_SOURCE_DIR}/CMake/sqlite3.cmake)
add_executable(TestSQLite)
target_compile_features(TestSQLite
PRIVATE
cxx_std_20
)
target_link_libraries(TestSQLite
PRIVATE
Garbage
sqlite3
Catch2::Catch2WithMain
)
target_sources(TestSQLite
PRIVATE
Main.cpp
)

35
Tests/SQLite/Main.cpp Normal file
View file

@ -0,0 +1,35 @@
#include <Garbage/SQLite.hpp>
using namespace Garbage::SQLite;
struct User
{
std::int64_t Id;
std::string Username;
std::string Email;
std::string PasswordHash;
};
Database db(
"UserDb",
Table(
"Users",
Column(&User::Id, "Id", ColumnFlags::PrimaryKey),
Column(&User::Username, "Username", ColumnFlags::Unique),
Column(&User::Email, "Email", ColumnFlags::Unique),
Column(&User::PasswordHash, "PasswordHash")
)
);
int main() {
User u {
.Id = 25,
.Username = "noob",
.Email = "noob@noob.io",
.PasswordHash = "ASDASDASDASDASDA",
};
Connection c(db, "Test.db");
c.Create();
c.Insert(u);
}

View file

@ -0,0 +1,30 @@
add_executable(TestSimpleConf)
target_compile_features(TestSimpleConf
PRIVATE
cxx_std_20
)
target_link_libraries(TestSimpleConf
PRIVATE
Garbage
Catch2::Catch2WithMain
)
target_sources(TestSimpleConf
PRIVATE
Comments.cpp
Conversions.cpp
EmptyConfigs.cpp
Files.cpp
Optionality.cpp
Parsing.cpp
WhitespaceBehaviour.cpp
)
add_custom_command(
TARGET TestSimpleConf POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/TestConfigs
${CMAKE_CURRENT_BINARY_DIR}/TestConfigs
)

View file

@ -0,0 +1,15 @@
#include <catch2/catch_test_macros.hpp>
#include <Garbage/SimpleConf.hpp>
TEST_CASE("Comments and empty lines are ignored")
{
using Required = Garbage::SimpleConf::Required;
using LookupError = Garbage::SimpleConfImplementation::LookupError;
std::filesystem::path path("TestConfigs/Comments.conf");
Garbage::SimpleConf config(path);
REQUIRE_THROWS_AS((config.Get<std::string, Required>("this shouldn't")), LookupError);
REQUIRE(config.Get<std::string, Required>("while this") == "is accessible");
}

View file

@ -0,0 +1,41 @@
#include <catch2/catch_test_macros.hpp>
#include <Garbage/SimpleConf.hpp>
TEST_CASE("Check if type conversions work correctly")
{
using Required = Garbage::SimpleConf::Required;
std::filesystem::path path("TestConfigs/BasicConversions.conf");
Garbage::SimpleConf config(path);
SECTION("Basic stuff that should be correct")
{
REQUIRE(config.Get<std::string, Required>("an int") == "123456");
REQUIRE(config.Get<std::string, Required>("a float") == "3.5");
REQUIRE(config.Get<std::string, Required>("a long (8B)") == "121212121212121212");
REQUIRE(config.Get<std::string, Required>("a double") == "0.0000128");
REQUIRE(config.Get<std::int32_t>("an int") == 123456);
REQUIRE(config.Get<float>("a float") == 3.5f);
REQUIRE(config.Get<std::int64_t>("a long (8B)") == 121212121212121212);
REQUIRE(config.Get<double>("a double") == 0.0000128);
}
SECTION("Stuff that should fail to parse")
{
REQUIRE(config.Get<std::string, Required>("an incorrect char") == "256");
REQUIRE(config.Get<std::string, Required>("unsigned") == "-34");
REQUIRE(config.Get<std::string, Required>("signed, but large") == "3000000000");
REQUIRE(config.Get<std::string, Required>("mangled float") == "3.f4");
REQUIRE(config.Get<std::string, Required>("mangled float 2") == "2.4f");
using ConversionError = Garbage::SimpleConfImplementation::ConversionError;
REQUIRE_THROWS_AS(config.Get<std::uint8_t>("an incorrect char"), ConversionError);
REQUIRE_THROWS_AS(config.Get<std::uint32_t>("unsigned"), ConversionError);
REQUIRE_THROWS_AS(config.Get<std::int32_t>("signed, but large"), ConversionError);
REQUIRE_THROWS_AS(config.Get<float>("mangled float"), ConversionError);
REQUIRE_THROWS_AS(config.Get<float>("mangled float 2"), ConversionError);
}
}

View file

@ -0,0 +1,17 @@
#include <catch2/catch_test_macros.hpp>
#include <Garbage/SimpleConf.hpp>
TEST_CASE("Config files with no key value pairs")
{
SECTION("Empty file")
{
std::filesystem::path path("TestConfigs/Empty.conf");
REQUIRE_NOTHROW(Garbage::SimpleConf(path));
}
SECTION("File with comments")
{
std::filesystem::path path("TestConfigs/CommentsOnly.conf");
REQUIRE_NOTHROW(Garbage::SimpleConf(path));
}
}

View file

@ -0,0 +1,23 @@
#include <catch2/catch_test_macros.hpp>
#include <Garbage/SimpleConf.hpp>
TEST_CASE("Interactions with files")
{
SECTION("Try loading a nonexistent file")
{
using ReadError = Garbage::SimpleConfImplementation::ReadError;
std::filesystem::path path("TestConfigs/ThisOneShouldntExist.conf");
REQUIRE_THROWS_AS(Garbage::SimpleConf(path), ReadError);
}
SECTION("Try loading a directory")
{
using ReadError = Garbage::SimpleConfImplementation::ReadError;
std::filesystem::path path("TestConfigs");
REQUIRE_THROWS_AS(Garbage::SimpleConf(path), ReadError);
}
}

View file

@ -0,0 +1,25 @@
#include <catch2/catch_test_macros.hpp>
#include <Garbage/SimpleConf.hpp>
TEST_CASE("Check handling of optional values")
{
std::filesystem::path path("TestConfigs/Optionality.conf");
Garbage::SimpleConf config(path);
SECTION("GetOptional behaviour")
{
REQUIRE(!config.GetOptional<std::string>("non-existent key"));
REQUIRE(*config.GetOptional<std::string>("existing key") == "existing value");
}
SECTION("Get behaviour")
{
using Required = Garbage::SimpleConf::Required;
using LookupError = Garbage::SimpleConfImplementation::LookupError;
REQUIRE(config.Get<std::string>("non-existent key", "a default") == "a default");
REQUIRE(config.Get<std::string>("existing key") == "existing value");
REQUIRE_THROWS_AS((config.Get<std::string, Required>("non-existent key")), LookupError);
}
}

View file

@ -0,0 +1,40 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <Garbage/SimpleConf.hpp>
TEST_CASE("Ensure robust parsing")
{
std::filesystem::path configs("TestConfigs");
using Catch::Matchers::ContainsSubstring;
SECTION("Multiple assignments")
{
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-MultipleEquals.conf"),
ContainsSubstring("[Line 3]"));
}
SECTION("Hash in key")
{
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-HashInKey.conf"),
ContainsSubstring("[Line 6]"));
}
SECTION("No equals")
{
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-OnlyKey.conf"),
ContainsSubstring("[Line 7]"));
}
SECTION("Empty key")
{
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-EmptyKey.conf"),
ContainsSubstring("[Line 7]"));
}
SECTION("Key at end of file")
{
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-KeyAtEnd.conf"),
ContainsSubstring("[Line 10]"));
}
}

View file

@ -0,0 +1,12 @@
# First, we wanna test if it can even do the basics
an int = 123456
a float = 3.5
a long (8B) = 121212121212121212
a double = 0.0000128
# Now we want to see if it correctly blows up
an incorrect char = 256
unsigned = -34
signed, but large = 3000000000
mangled float = 3.f4
mangled float 2 = 2.4f

View file

@ -0,0 +1,4 @@
# Test whether comments behave correctly
# this shouldn't = be accessible
while this = is accessible # and this comment doesn't interfere

View file

@ -0,0 +1,3 @@
# Another example of an empty config
# The config should initialize successfully,
# but there should be no kv-pairs inside it

View file

View file

@ -0,0 +1,2 @@
# This probably doesn't need to exist, but eh
existing key = existing value

View file

@ -0,0 +1,10 @@
# A simple config file that
normal_key=normal_value
another_key=another_value
generic=generic
another_one=another_value
= oh no, an empty key
another_one=...
# It doesn't really matter what's here, because the parser won't reach it

View file

@ -0,0 +1,9 @@
# A simple config file that contains a # character inside the key
uhh=
normal_key=normal_value
another_key=another_value
a_very_bas#ic_key=generic_value
another_one=...
# It doesn't really matter what's here, because the parser won't reach it

View file

@ -0,0 +1,10 @@
# A simple config file that
normal_key=normal_value
another_key=another_value
generic=generic
another_one=another_value
another_one=...
# It doesn't really matter what's here, because the parser won't reach it
sneaky_key

View file

@ -0,0 +1,4 @@
# A simple config
that appears = correct
but it = has = an invalid line # Should blow up here
another=pair

View file

@ -0,0 +1,10 @@
# A simple config file that contains only the key on a single line
normal_key=normal_value
another_key=another_value
generic=generic
another_one=another_value
a_very_basic_key
another_one=...
# It doesn't really matter what's here, because the parser won't reach it

View file

@ -0,0 +1,53 @@
#include <catch2/catch_test_macros.hpp>
#include <Garbage/SimpleConf.hpp>
TEST_CASE("Check behavior when whitespace is involved")
{
SECTION("Simple multi-line config")
{
using Required = Garbage::SimpleConf::Required;
std::string rawConfig =
" key with whitespace = and value too \n"
"another=one\n"
" and yet = another";
Garbage::SimpleConf config(std::move(rawConfig));
REQUIRE(config.Get<std::string, Required>("key with whitespace") == "and value too");
REQUIRE(config.Get<std::string, Required>("another") == "one");
REQUIRE(config.Get<std::string, Required>("and yet") == "another");
}
SECTION("Deal with CRLF correctly")
{
using Required = Garbage::SimpleConf::Required;
std::string rawConfig =
" simple key=simple value \r\n"
" another = one\r\n"
" and one = more\r\n";
Garbage::SimpleConf config(std::move(rawConfig));
REQUIRE(config.Get<std::string, Required>("simple key") == "simple value");
REQUIRE(config.Get<std::string, Required>("another") == "one");
REQUIRE(config.Get<std::string, Required>("and one") == "more");
}
SECTION("Weirder whitespace shenanigans")
{
using Required = Garbage::SimpleConf::Required;
std::string rawConfig =
"let's+start_simple=with some\tbasics\n"
"\t\tnow we get \tsome=\t \tweirder\tstuff\r\n"
"\t\tinsa+nity\twith\t\t=\t white \t space \t\t";
Garbage::SimpleConf config(std::move(rawConfig));
REQUIRE(config.Get<std::string, Required>("let's+start_simple") == "with some\tbasics");
REQUIRE(config.Get<std::string, Required>("now we get \tsome") == "weirder\tstuff");
REQUIRE(config.Get<std::string, Required>("insa+nity\twith") == "white \t space");
}
}