Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Based on [Glenn Fiedler's articles](https://gafferongames.com/post/reading_and_w
* [Half-precision float - half_precision](#half-precision-float---half_precision)
* [Bounded float - bounded_range](#bounded-float---bounded_range)
* [Quaternion - smallest_three\<Q, BitsPerElement\>](#quaternion---smallest_threeq-bitsperelement)
* [Checksum\<V\>](#checksumversion)
* [Extensibility](#extensibility)
* [Adding new serializables types](#adding-new-serializables-types)
* [Unified serialization](#unified-serialization)
Expand Down Expand Up @@ -443,6 +444,27 @@ bool status_write = writer.serialize<smallest_three<quaternion, 12>>(in_value);
bool status_read = reader.serialize<smallest_three<quaternion, 12>>(out_value);
```

## Checksum\<Version\>
A trait that creates a checksum based on the 32-bit number given.<br/>
If the checksum that was written does not match when reading, it returns false.
Must be called before anything else is serizalized, and again once everything is fully serialized.
When reading you can omit the last serialize, as it is a noop.

The call signature can be seen below:
```cpp
bool serialize<checksum<Version>>();
```
As well as a short example of its usage:
```cpp
bool status_write = writer.serialize<checksum<0x12345678>>();
// Serialize some stuff...
status_write = writer.serialize<checksum<0x12345678>>();

bool status_read = reader.serialize<checksum<0x12345678>>();
// Deserialize some stuff...
status_read = reader.serialize<checksum<0x12345678>>(); // Last deserialize on read is optional (a noop)
```

# Extensibility
The library is made with extensibility in mind.
The `bit_writer<T>` and `bit_reader<T>` use a template trait specialization of the given type to deduce how to serialize and deserialize the object.
Expand Down
2 changes: 1 addition & 1 deletion generate.bat
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
call vendor\bin\premake\premake5.exe vs2019
call vendor\bin\premake\premake5.exe vs2022
pause
1 change: 1 addition & 0 deletions include/bitstream/bitstream.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// Traits
#include "traits/array_traits.h"
#include "traits/bool_trait.h"
#include "traits/checksum_trait.h"
#include "traits/enum_trait.h"
#include "traits/float_trait.h"
#include "traits/integral_traits.h"
Expand Down
29 changes: 0 additions & 29 deletions include/bitstream/stream/bit_reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,35 +106,6 @@ namespace bitstream
*/
[[nodiscard]] uint32_t get_total_bits() const noexcept { return m_Policy.get_total_bits(); }

/**
* @brief Reads the first 32 bits of the buffer and compares it to a checksum of the @p protocol_version and the rest of the buffer
* @param protocol_version A unique version number
* @return Whether the checksum matches what was written
*/
[[nodiscard]] bool serialize_checksum(uint32_t protocol_version) noexcept
{
BS_ASSERT(get_num_bits_serialized() == 0);

BS_ASSERT(can_serialize_bits(32U));

uint32_t num_bytes = (get_total_bits() - 1U) / 8U + 1U;
const uint32_t* buffer = m_Policy.get_buffer();

// Generate checksum to compare against
uint32_t generated_checksum = utility::crc_uint32(reinterpret_cast<const uint8_t*>(&protocol_version), reinterpret_cast<const uint8_t*>(buffer + 1), num_bytes - 4);

// Advance the reader by the size of the checksum (32 bits / 1 word)
m_WordIndex++;

BS_ASSERT(m_Policy.extend(32U));

// Read the checksum
uint32_t checksum = *buffer;

// Compare the checksum
return generated_checksum == checksum;
}

/**
* @brief Pads the buffer up to the given number of bytes
* @param num_bytes The byte number to pad to
Expand Down
43 changes: 2 additions & 41 deletions include/bitstream/stream/bit_writer.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,46 +126,6 @@ namespace bitstream
return get_num_bits_serialized();
}

/**
* @brief Instructs the writer that you intend to use `serialize_checksum()` later on, and to reserve the first 32 bits.
* @return Returns false if anything has already been written to the buffer or if there's no space to write the checksum
*/
[[nodiscard]] bool prepend_checksum() noexcept
{
BS_ASSERT(get_num_bits_serialized() == 0);

BS_ASSERT(m_Policy.extend(32U));

// Advance the reader by the size of the checksum (32 bits / 1 word)
m_WordIndex++;

return true;
}

/**
* @brief Writes a checksum of the @p protocol_version and the rest of the buffer as the first 32 bits
* @param protocol_version A unique version number
* @return The number of bytes written to the buffer
*/
uint32_t serialize_checksum(uint32_t protocol_version) noexcept
{
uint32_t num_bits = flush();

BS_ASSERT(num_bits > 32U);

// Copy protocol version to buffer
uint32_t* buffer = m_Policy.get_buffer();
*buffer = protocol_version;

// Generate checksum of version + data
uint32_t checksum = utility::crc_uint32(reinterpret_cast<uint8_t*>(buffer), get_num_bytes_serialized());

// Put checksum at beginning
*buffer = checksum;

return num_bits;
}

/**
* @brief Pads the buffer up to the given number of bytes with zeros
* @param num_bytes The byte number to pad to
Expand Down Expand Up @@ -338,7 +298,8 @@ namespace bitstream
* @param writer The writer to copy into
* @return Returns false if writing would overflow the buffer
*/
[[nodiscard]] bool serialize_into(bit_writer& writer) const noexcept
template<typename T>
[[nodiscard]] bool serialize_into(bit_writer<T>& writer) const noexcept
{
uint8_t* buffer = reinterpret_cast<uint8_t*>(m_Policy.get_buffer());
uint32_t num_bits = get_num_bits_serialized();
Expand Down
78 changes: 78 additions & 0 deletions include/bitstream/traits/checksum_trait.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/crc.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"

#include "../stream/serialize_traits.h"

namespace bitstream
{
/**
* @brief Type for checksums
* @tparam Version A unique version number
*/
template<uint32_t Version>
struct checksum;

/**
* @brief A trait used to serialize a checksum of the @p Version and the rest of the buffer as the first 32 bits.
* This should be called both first and last when reading and writing to a buffer.
* @tparam Version A unique version number
*/
template<uint32_t Version>
struct serialize_traits<checksum<Version>>
{
constexpr static uint32_t protocol_version = utility::to_big_endian32_const(Version);
constexpr static uint32_t protocol_size = sizeof(uint32_t);

template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer) noexcept
{
if (writer.get_num_bits_serialized() == 0)
return writer.pad_to_size(4);

uint32_t num_bits = writer.flush();

BS_ASSERT(num_bits >= 32U);

// Get buffer info
uint8_t* byte_buffer = writer.get_buffer();
uint32_t num_bytes = writer.get_num_bytes_serialized();

// Generate checksum of version + data
uint32_t generated_checksum = utility::crc_uint32(protocol_version, byte_buffer + protocol_size, writer.get_num_bytes_serialized() - protocol_size);

// Put checksum at beginning
uint32_t* buffer = reinterpret_cast<uint32_t*>(byte_buffer);
*buffer = utility::to_big_endian32(generated_checksum);

return true;
}

template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader) noexcept
{
if (reader.get_num_bits_serialized() > 0)
return true;

BS_ASSERT(reader.can_serialize_bits(32U));

// Get buffer info
const uint8_t* byte_buffer = reader.get_buffer();
uint32_t num_bytes = (reader.get_total_bits() - 1U) / 8U + 1U;

// Generate checksum to compare against
uint32_t generated_checksum = utility::crc_uint32(protocol_version, byte_buffer + protocol_size, num_bytes - protocol_size);

// Read the checksum
uint32_t given_checksum;
BS_ASSERT(reader.serialize_bits(given_checksum, 32U));

// Compare the checksum
return generated_checksum == given_checksum;
}
};
}
16 changes: 5 additions & 11 deletions include/bitstream/utility/crc.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <array>
#include <cstdint>
#include <cstring>

namespace bitstream::utility
{
Expand All @@ -22,22 +23,15 @@ namespace bitstream::utility
return table;
}();

inline constexpr uint32_t crc_uint32(const uint8_t* bytes, uint32_t size)
inline uint32_t crc_uint32(uint32_t checksum, const uint8_t* bytes, uint32_t size)
{
uint32_t result = 0xFFFFFFFF;

for (uint32_t i = 0; i < size; i++)
result = CHECKSUM_TABLE[(result & 0xFF) ^ *(bytes + i)] ^ (result >> 8);

return ~result;
}

inline constexpr uint32_t crc_uint32(const uint8_t* checksum, const uint8_t* bytes, uint32_t size)
{
uint32_t result = 0xFFFFFFFF;
uint8_t checksum_table[4]{};
std::memcpy(&checksum_table, &checksum, sizeof(uint32_t));

for (uint32_t i = 0; i < 4; i++)
result = CHECKSUM_TABLE[(result & 0xFF) ^ *(checksum + i)] ^ (result >> 8);
result = CHECKSUM_TABLE[(result & 0xFF) ^ *(checksum_table + i)] ^ (result >> 8);

for (uint32_t i = 0; i < size; i++)
result = CHECKSUM_TABLE[(result & 0xFF) ^ *(bytes + i)] ^ (result >> 8);
Expand Down
36 changes: 29 additions & 7 deletions include/bitstream/utility/endian.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "platform.h"

#include <cstdint>

#if defined(__cpp_lib_endian) && __cpp_lib_endian >= 201907L
Expand Down Expand Up @@ -63,23 +65,43 @@ namespace bitstream::utility
#endif // defined(BS_LITTLE_ENDIAN)
}

inline uint32_t endian_swap32(uint32_t value)
constexpr inline uint32_t endian_swap32_const(uint32_t value)
{
#if defined(_WIN32)
return _byteswap_ulong(value);
#elif defined(__linux__)
return __builtin_bswap32(value);
#else
const uint32_t first = (value << 24) & 0xFF000000;
const uint32_t second = (value << 8) & 0x00FF0000;
const uint32_t third = (value >> 8) & 0x0000FF00;
const uint32_t fourth = (value >> 24) & 0x000000FF;

return first | second | third | fourth;
}

BS_CONSTEXPR inline uint32_t endian_swap32(uint32_t value)
{
if BS_CONST_EVALUATED()
{
return endian_swap32_const(value);
}
else
{
#if defined(_WIN32)
return _byteswap_ulong(value);
#elif defined(__linux__)
return __builtin_bswap32(value);
#else
return endian_swap32_const(value);
#endif // _WIN32 || __linux__
}
}

constexpr inline uint32_t to_big_endian32_const(uint32_t value)
{
if constexpr (little_endian())
return endian_swap32_const(value);
else
return value;
}

inline uint32_t to_big_endian32(uint32_t value)
BS_CONSTEXPR inline uint32_t to_big_endian32(uint32_t value)
{
if constexpr (little_endian())
return endian_swap32(value);
Expand Down
7 changes: 1 addition & 6 deletions include/bitstream/utility/parameter.h
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
#pragma once

#include "assert.h"
#include "platform.h"

#include <utility>
#include <type_traits>

#ifdef __cpp_constexpr_dynamic_alloc
#define BS_CONSTEXPR constexpr
#else // __cpp_constexpr_dynamic_alloc
#define BS_CONSTEXPR
#endif // __cpp_constexpr_dynamic_alloc

namespace bitstream
{
#ifdef BS_DEBUG_BREAK
Expand Down
11 changes: 11 additions & 0 deletions include/bitstream/utility/platform.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include <type_traits>

#if defined(__cpp_lib_is_constant_evaluated) && __cpp_lib_is_constant_evaluated >= 201811L
# define BS_CONST_EVALUATED() (std::is_constant_evaluated())
# define BS_CONSTEXPR constexpr
#else // __cpp_lib_is_constant_evaluated
# define BS_CONST_EVALUATED() constexpr (false)
# define BS_CONSTEXPR
#endif // __cpp_lib_is_constant_evaluated
Loading