Marser is a lightweight, high-performance data serialization library built with C++20.
It provides a type-safe, JSON-like structure for managing configuration data (or any other data you need), featuring "dotted.path" navigation, and built-in encryption modules.
Marser doesn't just store data; it secures it. The 'FilePacker' and 'FileLoader' modules implement a custom binary format, to ensure your configuration remains private and uncorrupted.
- Ciphers Built-in Xor and Shift encryption strategies.
- Integrity Verification Every file includes Checksum CRC and Magic Header
- Corruption Detection The 'FileLoader' validates the checksum and header before parsing, preventing crashes from corrupted files.
Marser utilizies a Top Down Parser coupled with a custom Tokenizer
- Tokenization The source text is broken down into vector of identified symbols.
- Recursive Logic Recursion is used to walk the token stream.
- Error Handling The parser validates syntax as it goes, ensuring that invalid symbols are caught early.
Marser relies on modern C++ to ensure zero unnecessary allocations, type safety and good performance.
- Value is implemented as a std::variant. This allows to handle multiple data types within a single object while preventing 'type-confusion' bugs.
- Minimized heap allocations Objects like std::string_view or std::span are used extensively throughout the code to reference existing memory, rather than copying it.
- Dotted.Path takes advantage of functionalities like std::ranges::views::split and std::from_chars to navigate the deep data structure without any copies.
You can use Marser to read configuration files, but not only. The library provides functionalities, such as saving files with encryption mechanism, loading them back and decrypting. You might also create your own Value map, pack it and save to file.
- Basic Value creation and dotted.path access
#include <iostream>
#include "parser/Value.h"
//Basic Value object creation
//Dotted.path access
int main()
{
//Auto Vivification - you can create maps and lists as long as you assign values
matt::parser::Value root;
root["server"]["ip"] = matt::parser::Value(std::string("127.0.0.1"));
root["server"]["ports"] = matt::parser::Value::List{ matt::parser::Value{80}, matt::parser::Value{443}, matt::parser::Value{8080} };
//Example of using the 'dotted.path' navigation
auto connectionIp = root.get("server.ip").asString();
auto primaryPort = root.get("server.ports.0").asInt();
std::cout << "Connection Ip is: " << connectionIp << '\n';
std::cout << "Primary port is: " << primaryPort << '\n';
/*
Output result:
Connection Ip is: 127.0.0.1
Primary port is: 80
*/
return 0;
}- Using MattFile class to save and load content.
#include <iostream>
#include "parser/Value.h"
#include "io/MattFile.h"
//Saving with encryption
//Loading files
int main()
{
//Auto Vivification - you can create maps and lists as long as you assign values
matt::parser::Value root;
root["server"]["ip"] = matt::parser::Value(std::string("127.0.0.1"));
root["server"]["ports"] = matt::parser::Value::List{ matt::parser::Value{80}, matt::parser::Value{443}, matt::parser::Value{8080} };
//Get string object of value
std::string rootString = root.emitString();
//Save content to file with XOR Encryption
//Key is saved inside 'KeyVault' class.
bool contentSaved = matt::io::MattFile::saveContent(rootString, "rootExample1.matt", matt::encryption::EncryptionType::Xor);
std::cout << "Content saved status: " << std::boolalpha << contentSaved << '\n';
//Load content back (MattFile manages decryption automatically)
std::string rootContent = matt::io::MattFile::loadAsText("rootExample1.matt");
bool rootCotentEqualsRootString = rootString == rootContent;
std::cout << "Loaded content is the same: " << rootCotentEqualsRootString << '\n';
/*
Output result:
Content saved status: true
[INFO] FileLoader.cpp:loadFromFile():17 Loading package from file: "rootExample1.matt"
Loaded content is the same: true
*/
return 0;
}- Using Parser functionality to get an easy access to all the data.
#include <iostream>
#include "parser/Parser.h"
//Parsing data example
int main()
{
//Raw data (supports comments)
std::string_view rawData = R"(
# Connection Settings
host: "127.0.0.1"
port: 8080
timeout: 5.5
)";
//You can use matt::parser::Parser::parseFile to read from file (automatically decrypts if its MATT File).
matt::parser::Value parsedData = matt::parser::Parser::parseContent(rawData);
std::cout << "Host is: " << parsedData["host"].asString() << std::endl;
std::cout << "Port is: " << parsedData["port"].asInt() << std::endl;
std::cout << "Timeout is: " << parsedData["timeout"].asDouble() << std::endl;
/*
* Output result is:
Host is: 127.0.0.1
Port is: 8080
Timeout is: 5.5
*/
return 0;
}Marser is designed to be modular. You can add a custom encryption algorithm by following three simple steps:
- Inherit from IEncryptionAlgorithm: Create a new class that overrides the virtual methods.
- Register your Type: Create your variable of type EncryptionType (matt::encryption::EncryptionType)
- Register your new algorithm by calling matt::encryption::EncryptionRegistry::registerAlgorithm function.
#include <iostream>
#include "encryption/EncryptionRegistry.h"
//Create id of type EncryptionType (uint8_t), with the value (preferably) > 100
matt::encryption::EncryptionType myCustomAlgorithmType = 101;
//Make your custom class that inherits from IEncryptionAlgorithm
class MyCustomAlgorithm : public matt::encryption::IEncryptionAlgorithm
{
public:
MyCustomAlgorithm(std::span<const std::byte> masterKey)
:IEncryptionAlgorithm(masterKey)
{
}
// Inherited via IEncryptionAlgorithm
virtual matt::encryption::ByteVector encode(std::span<const std::byte> bytes) const override
{
//Your custom encrypting code;
return {};
}
virtual matt::encryption::ByteVector decode(std::span<const std::byte> bytes) const override
{
//Here your custom decrypting code.
return {};
}
virtual matt::encryption::EncryptionType getType() const override
{
return myCustomAlgorithmType;
}
};
int main()
{
//We have our MyCustomAlgorithm class, so we can register it
matt::encryption::EncryptionRegistry::registerAlgorithm(myCustomAlgorithmType,
[](std::span<const std::byte> key1, std::span<const std::byte> key2) {
return std::make_unique<MyCustomAlgorithm>(key1);
});
//Now algorithm is registered!
auto algorithm = matt::encryption::EncryptionRegistry::getAlgorithm(myCustomAlgorithmType, {}, {});
if (algorithm)
std::cout << "Algorithm registered!\n";
else
std::cout << "Failed to register algorithm.\n";
}Aside from adding custom algorithms for encryption, you should also save your new master key for the algorithm. Class KeyVault will help you with that.
#include <iostream>
#include "encryption/KeyVault.h"
#include "parser/Parser.h"
#include "io/MattFile.h"
//How to add new key to the vault and use it later.
int main()
{
//Registering new key
constexpr uint8_t myCustomAlgorithmType = 101;
matt::encryption::ByteVector myCustomKey = matt::encryption::ByteVector
{
std::byte{'M'}, std::byte{'Y'}, std::byte{'K'}, std::byte{'E'}, std::byte{'Y'}
};
matt::encryption::KeyVault keyVault;
keyVault.addKey(myCustomAlgorithmType, myCustomKey);
//Now after keyVault has your custom key, you can use it by passing keyVault class to the Parser
//keyVault will be used to find the proper key.
matt::parser::Parser::parseFile("myFile.txt", &keyVault);
//Also you might use it to pack data.
matt::io::MattFile::saveContent("MyContent", "Result.matt", myCustomAlgorithmType, &keyVault);
//If you used custom key to save file, you are required also to use the same keyVault when loading;
matt::io::MattFile::loadAsText("Result.matt", &keyVault);
}Once implemented, the FilePacker and FileLoader will automatically be able to use your new algorithm, without any change to core logic.
Marser uses CMAKE (>= 3.15) for building, and integrates GoogleTests for running unit tests.
- Open command terminal (cmd)
- Navigate to where you want to clone the project (cd C:/Projects)
- Clone the repository (git clone --recursive https://github.com/matheoheo/Marser.git)
- Enter the project folder (cd Marser)
- Create build directory and navigate there (mkdir build & cd build)
- Configure the project (cmake ..)
- Compile the project (cmake --build .)
Now you can run tests by typing the command ctest from build folder level, or you can navigate to /bin/Debug with cd bin/Debug and run MarserTests.exe