Initial decoder implementation, including a basic two-way test

The implementation is still far from perfect, it's at beast a proof of concept. There are many edge cases that are definitely still not covered, and many rough edges in the quality of the code.

I am also not convinced that exceptions are the best error handling method for this, particularly for publicly exposes interfaces that may be much more susceptible to DoS attacks due to malformed input (and the resulting overhead in handling exceptions).
This commit is contained in:
TennesseeTrash 2025-09-18 23:23:33 +02:00
parent 4159fc4643
commit 11636af323
9 changed files with 1431 additions and 112 deletions

View file

@ -5,6 +5,13 @@ target_compile_features(LibCBOR
cxx_std_23
)
target_compile_options(LibCBOR PUBLIC
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic -Wold-style-cast -Wcast-align -Wunused
-Woverloaded-virtual -Wconversion -Wsign-conversion -Wformat=2
-Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough>
)
target_include_directories(LibCBOR
PUBLIC
"Include"
@ -15,6 +22,7 @@ target_include_directories(LibCBOR
target_sources(LibCBOR
PRIVATE
"Source/Core.cpp"
"Source/Decoder.cpp"
"Source/Encoder.cpp"
)

View file

@ -49,49 +49,52 @@ namespace CBOR
String = 0b0110'0000,
Array = 0b1000'0000,
Map = 0b1010'0000,
Tagged = 0b1100'0000,
Tag = 0b1100'0000,
Other = 0b1110'0000,
TypeMask = 0b1110'0000,
};
std::string_view ToString(MajorType type);
enum class ArgumentPosition: std::uint8_t
{
Direct00 = 0b0000'0000,
Direct01 = 0b0000'0001,
Direct02 = 0b0000'0010,
Direct03 = 0b0000'0011,
Direct04 = 0b0000'0100,
Direct05 = 0b0000'0101,
Direct06 = 0b0000'0110,
Direct07 = 0b0000'0111,
Direct08 = 0b0000'1000,
Direct09 = 0b0000'1001,
Direct10 = 0b0000'1010,
Direct11 = 0b0000'1011,
Direct12 = 0b0000'1100,
Direct13 = 0b0000'1101,
Direct14 = 0b0000'1110,
Direct15 = 0b0000'1111,
Direct16 = 0b0001'0000,
Direct17 = 0b0001'0001,
Direct18 = 0b0001'0010,
Direct19 = 0b0001'0011,
Direct21 = 0b0001'0101,
Direct20 = 0b0001'0100,
Direct23 = 0b0001'0111,
Direct22 = 0b0001'0110,
Direct00 = 0b0000'0000,
Direct01 = 0b0000'0001,
Direct02 = 0b0000'0010,
Direct03 = 0b0000'0011,
Direct04 = 0b0000'0100,
Direct05 = 0b0000'0101,
Direct06 = 0b0000'0110,
Direct07 = 0b0000'0111,
Direct08 = 0b0000'1000,
Direct09 = 0b0000'1001,
Direct10 = 0b0000'1010,
Direct11 = 0b0000'1011,
Direct12 = 0b0000'1100,
Direct13 = 0b0000'1101,
Direct14 = 0b0000'1110,
Direct15 = 0b0000'1111,
Direct16 = 0b0001'0000,
Direct17 = 0b0001'0001,
Direct18 = 0b0001'0010,
Direct19 = 0b0001'0011,
Direct20 = 0b0001'0100,
Direct21 = 0b0001'0101,
Direct22 = 0b0001'0110,
Direct23 = 0b0001'0111,
Next1B = 0b0001'1000,
Next2B = 0b0001'1001,
Next4B = 0b0001'1010,
Next8B = 0b0001'1011,
Next1B = 0b0001'1000,
Next2B = 0b0001'1001,
Next4B = 0b0001'1010,
Next8B = 0b0001'1011,
Reserved28 = 0b0001'1100,
Reserved29 = 0b0001'1101,
Reserved30 = 0b0001'1110,
Reserved28 = 0b0001'1100,
Reserved29 = 0b0001'1101,
Reserved30 = 0b0001'1110,
Indefinite = 0b0001'1111,
Indefinite = 0b0001'1111,
PositionMask = 0b0001'1111,
};
enum class MinorType: std::uint8_t

View file

@ -3,6 +3,8 @@
#include "Core.hpp"
#include <span>
namespace CBOR
{
class DecodeError: public Error
@ -11,16 +13,207 @@ namespace CBOR
using Error::Error;
};
// Forward decl
class Decoder;
class Item
{
private:
Item(Decoder &decoder);
public:
bool Bool ();
Special Special();
std::int8_t Int8 ();
std::int16_t Int16 ();
std::int32_t Int32 ();
std::int64_t Int64 ();
std::uint8_t Uint8 ();
std::uint16_t Uint16 ();
std::uint32_t Uint32 ();
std::uint64_t Uint64 ();
// Note(3011): float16_t is currently not supported
float Float ();
double Double ();
class Binary Binary ();
class String String ();
class Array Array ();
class Map Map ();
private:
friend class Decoder;
friend class Array;
friend class KeyValue;
Decoder *mDecoder;
};
class KeyValue
{
private:
KeyValue(Decoder &decoder);
public:
Item Key();
Item Value();
private:
friend class Decoder;
friend class Map;
enum class State
{
Initial, KeyPulled, Done,
};
State mState;
Decoder *mDecoder;
};
class Binary
{
private:
Binary(Decoder &decoder);
public:
std::span<std::uint8_t> Get();
void AllowIndefinite();
bool Done();
std::span<std::uint8_t> Next();
private:
friend class Decoder;
bool mHeaderParsed;
bool mIndefiniteAllowed;
bool mDone;
bool mCheckedDone;
Decoder *mDecoder;
};
class String
{
private:
String(Decoder &decoder);
public:
std::string_view Get();
void AllowIndefinite();
bool Done();
std::string_view Next();
private:
friend class Decoder;
bool mHeaderParsed;
bool mIndefiniteAllowed;
bool mDone;
bool mCheckedDone;
Decoder *mDecoder;
};
class Array
{
private:
Array(Decoder &decoder);
public:
bool Done();
Item Next();
private:
static constexpr std::size_t Indefinite = std::numeric_limits<std::size_t>::max();
friend class Decoder;
bool mHeaderParsed;
bool mDone;
bool mCheckedDone;
std::size_t mCurrent;
std::size_t mSize;
Decoder *mDecoder;
};
class Map
{
private:
Map(Decoder &decoder);
public:
bool Done();
KeyValue Next();
private:
static constexpr std::size_t Indefinite = std::numeric_limits<std::size_t>::max();
friend class Decoder;
bool mHeaderParsed;
bool mDone;
bool mCheckedDone;
std::size_t mCurrent;
std::size_t mSize;
Decoder *mDecoder;
};
class Decoder
{
public:
private:
};
Decoder(std::span<std::uint8_t> buffer);
bool Bool ();
Special Special();
std::int8_t Int8 ();
std::int16_t Int16 ();
std::int32_t Int32 ();
std::int64_t Int64 ();
std::uint8_t Uint8 ();
std::uint16_t Uint16 ();
std::uint32_t Uint32 ();
std::uint64_t Uint64 ();
// Note(3011): float16_t is currently not supported
float Float ();
double Double ();
Binary Binary ();
String String ();
Array Array ();
Map Map ();
class Validator
{
public:
private:
friend class Binary;
friend class String;
friend class Array;
friend class Map;
enum class State: std::uint8_t
{
Initial,
HeaderExtracted,
};
struct Header
{
MajorType Type;
MinorType Minor;
ArgumentPosition ArgPosition;
std::uint64_t Argument;
};
Header PeekHeader();
Header ExtractHeader();
std::span<std::uint8_t> ExtractBinary(std::size_t size);
std::string_view ExtractString(std::size_t size);
State mState;
std::size_t mCurrent;
std::span<std::uint8_t> mBuffer;
};
}

View file

@ -56,6 +56,8 @@ namespace CBOR
void BeginIndefiniteMap();
void End();
std::size_t EncodedSize() const;
private:
std::size_t mCurrent;
std::span<std::uint8_t> mBuffer;

30
LibCBOR/Source/Core.cpp Normal file
View file

@ -0,0 +1,30 @@
#include "Core.hpp"
#include <utility>
namespace CBOR
{
std::string_view ToString(MajorType type)
{
switch (type) {
case MajorType::Unsigned:
return "unsigned";
case MajorType::Negative:
return "negative";
case MajorType::Binary:
return "binary";
case MajorType::String:
return "string";
case MajorType::Array:
return "array";
case MajorType::Map:
return "map";
case MajorType::Tag:
return "tag";
case MajorType::Other:
return "other";
}
std::unreachable();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
#include "Encoder.hpp"
#include "Core.hpp"
#include "Utils.hpp"
@ -133,26 +134,26 @@ namespace CBOR
if (value >= 0) {
if (value <= 23) {
EnsureEnoughSpace(mBuffer, mCurrent, 1);
mBuffer[mCurrent++] = std::to_underlying(MajorType::Unsigned) | value;
mBuffer[mCurrent++] = static_cast<std::uint8_t>(std::to_underlying(MajorType::Unsigned) | value);
}
else {
EnsureEnoughSpace(mBuffer, mCurrent, 2);
mBuffer[mCurrent++] = std::to_underlying(MajorType::Unsigned)
| std::to_underlying(ArgumentPosition::Next1B);
mBuffer[mCurrent++] = value;
mBuffer[mCurrent++] = static_cast<std::uint8_t>(value);
}
}
else {
std::int8_t actual = std::abs(value + 1);
std::int8_t actual = -(value + 1);
if (actual <= 23) {
EnsureEnoughSpace(mBuffer, mCurrent, 1);
mBuffer[mCurrent++] = std::to_underlying(MajorType::Negative) | actual;
mBuffer[mCurrent++] = static_cast<std::uint8_t>(std::to_underlying(MajorType::Negative) | actual);
}
else {
EnsureEnoughSpace(mBuffer, mCurrent, 2);
mBuffer[mCurrent++] = std::to_underlying(MajorType::Negative)
| std::to_underlying(ArgumentPosition::Next1B);
mBuffer[mCurrent++] = actual;
mBuffer[mCurrent++] = static_cast<std::uint8_t>(actual);
}
}
}
@ -166,7 +167,7 @@ namespace CBOR
Encode(static_cast<std::uint8_t>(value));
}
else if (value >= -256 && value <= -1) {
std::uint8_t actual = static_cast<std::uint8_t>(std::abs(value + 1));
std::uint8_t actual = static_cast<std::uint8_t>(-(value + 1));
EnsureEnoughSpace(mBuffer, mCurrent, 2);
mBuffer[mCurrent++] = std::to_underlying(MajorType::Negative)
| std::to_underlying(ArgumentPosition::Next1B);
@ -180,10 +181,10 @@ namespace CBOR
}
else {
EnsureEnoughSpace(mBuffer, mCurrent, 3);
std::int16_t actual = std::abs(value + 1);
std::int16_t actual = -(value + 1);
mBuffer[mCurrent++] = std::to_underlying(MajorType::Negative)
| std::to_underlying(ArgumentPosition::Next2B);
Write(mBuffer, mCurrent, static_cast<std::uint16_t>(value));
Write(mBuffer, mCurrent, static_cast<std::uint16_t>(actual));
}
}
@ -475,4 +476,9 @@ namespace CBOR
mBuffer[mCurrent++] = std::to_underlying(MajorType::Other)
| std::to_underlying(MinorType::Break);
}
std::size_t BasicEncoder::EncodedSize() const
{
return mBuffer.size() - (mBuffer.size() - mCurrent);
}
}

View file

@ -3,52 +3,9 @@
#include <bit>
#include <concepts>
#include <cstdint>
namespace CBOR
{
[[nodiscard, deprecated("Prefer HostToNetwork or NetworkToHost")]] constexpr
std::uint8_t FlipBytes(std::uint8_t value)
{
return value;
}
[[nodiscard, deprecated("Prefer HostToNetwork or NetworkToHost")]] constexpr
std::uint16_t FlipBytes(std::uint16_t value)
{
static constexpr std::uint16_t upperMask = 0b1111'1111'0000'0000;
static constexpr std::uint16_t lowerMask = 0b0000'0000'1111'1111;
return (value & upperMask) >> 8 | (value & lowerMask) << 8;
}
[[nodiscard, deprecated("Prefer HostToNetwork or NetworkToHost")]] constexpr
std::uint32_t FlipBytes(std::uint32_t value)
{
static constexpr std::uint32_t firstMask = 0xFF'00'00'00;
static constexpr std::uint32_t secondMask = 0x00'FF'00'00;
static constexpr std::uint32_t thirdMask = 0x00'00'FF'00;
static constexpr std::uint32_t fourthMask = 0x00'00'00'FF;
return (value & firstMask) >> 24 | (value & secondMask) >> 8 |
(value & thirdMask) << 8 | (value & fourthMask) << 24;
}
[[nodiscard, deprecated("Prefer HostToNetwork or NetworkToHost")]] constexpr
std::uint64_t FlipBytes(std::uint64_t value)
{
static constexpr std::uint64_t firstMask = 0xFF'00'00'00'00'00'00'00;
static constexpr std::uint64_t secondMask = 0x00'FF'00'00'00'00'00'00;
static constexpr std::uint64_t thirdMask = 0x00'00'FF'00'00'00'00'00;
static constexpr std::uint64_t fourthMask = 0x00'00'00'FF'00'00'00'00;
static constexpr std::uint64_t fifthMask = 0x00'00'00'00'FF'00'00'00;
static constexpr std::uint64_t sixthMask = 0x00'00'00'00'00'FF'00'00;
static constexpr std::uint64_t seventhMask = 0x00'00'00'00'00'00'FF'00;
static constexpr std::uint64_t eighthMask = 0x00'00'00'00'00'00'00'FF;
return (value & firstMask ) >> 56 | (value & secondMask) >> 40 |
(value & thirdMask ) >> 24 | (value & fourthMask) >> 8 |
(value & fifthMask ) << 8 | (value & sixthMask ) >> 24 |
(value & seventhMask) << 40 | (value & eighthMask) << 56;
}
template <std::integral T>
[[nodiscard]] constexpr
T HostToNetwork(T value)

View file

@ -1,39 +1,156 @@
#include "CBOR/Core.hpp"
#include "CBOR/Decoder.hpp"
#include "CBOR/Encoder.hpp"
#include <array>
#include <cstdint>
#include <print>
#include <ranges>
#include <stdexcept>
#include <vector>
struct SomeStruct
{
std::string name;
double speed;
float fov;
std::int8_t thing;
std::int64_t slots;
std::uint32_t times;
std::vector<std::string> tools;
};
std::size_t Encode(const SomeStruct &value, std::span<std::uint8_t> buffer)
{
CBOR::BasicEncoder enc(buffer);
enc.BeginIndefiniteMap();
enc.Encode("name");
enc.Encode(value.name);
enc.Encode("speed");
enc.Encode(value.speed);
enc.Encode("fov");
enc.Encode(value.fov);
enc.Encode("thing");
enc.Encode(value.thing);
enc.Encode("slots");
enc.Encode(value.slots);
enc.Encode("times");
enc.Encode(value.times);
enc.Encode("tools");
enc.BeginIndefiniteArray();
for (std::string_view tool: value.tools) {
enc.Encode(tool);
}
enc.End();
enc.End();
return enc.EncodedSize();
}
SomeStruct Decode(std::span<std::uint8_t> buffer)
{
SomeStruct result;
CBOR::Decoder dec(buffer);
CBOR::Map object = dec.Map();
while (!object.Done()) {
CBOR::KeyValue kv = object.Next();
std::string_view key = kv.Key().String().Get();
CBOR::Item value = kv.Value();
if (key == "name") {
result.name = value.String().Get();
}
else if (key == "speed") {
result.speed = value.Double();
}
else if (key == "fov") {
result.fov = value.Float();
}
else if (key == "thing") {
result.thing = value.Int8();
}
else if (key == "slots") {
result.slots = value.Int64();
}
else if (key == "times") {
result.times = value.Uint32();
}
else if (key == "tools") {
CBOR::Array tools = value.Array();
while(!tools.Done()) {
result.tools.push_back(std::string(tools.Next().String().Get()));
}
}
}
return result;
}
void Compare(const SomeStruct &s1, const SomeStruct &s2)
{
if (s1.name != s2.name) {
throw std::runtime_error("test error: names are not the same");
}
if (s1.speed != s2.speed) {
throw std::runtime_error("test error: speed is not the same");
}
if (s1.fov != s2.fov) {
throw std::runtime_error("test error: fovs are not the same");
}
if (s1.thing != s2.thing) {
throw std::runtime_error("test error: things are not the same");
}
if (s1.slots != s2.slots) {
throw std::runtime_error("test error: slots are not the same");
}
if (s1.times != s2.times) {
throw std::runtime_error("test error: times are not the same");
}
for (const auto &[t1, t2]: std::ranges::views::zip(s1.tools, s2.tools)) {
if (t1 != t2) {
throw std::runtime_error("test error: some tools are not the same");
}
}
}
int main()
{
using namespace std::string_view_literals;
std::array<std::uint8_t, 256> buffer = {0};
std::array<std::uint8_t, 1024> buffer = {0};
std::vector<std::uint8_t> binData(5, 'g');
SomeStruct expected {
.name = "Player1",
.speed = 5.0,
.fov = 110.0f,
.thing = -15,
.slots = 40'000'000,
.times = 1234567,
.tools = {
"pickaxe",
"sword",
"axe",
"magical arrow",
"iron ore",
},
};
CBOR::BasicEncoder enc(buffer);
try {
std::size_t encodedSize = Encode(expected, buffer);
std::println("Encoded size: {}", encodedSize);
enc.BeginMap(7);
enc.Encode("Hello ");
enc.Encode("World! ");
enc.Encode("Behold by new power! ");
enc.Encode("It truly is a sight to see ..."sv);
enc.Encode(std::int64_t(1212121212121212));
enc.Encode(binData);
enc.Encode("random double");
enc.Encode(420.69);
enc.Encode("random float");
enc.Encode(420.69f);
enc.Encode("undefined?");
enc.Encode(CBOR::Special::Undefined);
enc.Encode("null?");
enc.Encode(CBOR::Special::Null);
enc.End();
SomeStruct result = Decode(std::span<std::uint8_t>(buffer.data(), encodedSize));
for (const auto &byte: buffer) {
std::print("{:02x} ", byte);
Compare(expected, result);
std::println("The test has been completed successfully.");
}
catch (const std::exception &e) {
std::println("Error: {}", e.what());
}
std::println("");
}