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
14 changes: 14 additions & 0 deletions include/glaze/beve/header.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
#include <array>
#include <bit>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iterator>

#include "glaze/concepts/container_concepts.hpp"
#include "glaze/core/context.hpp"
#include "glaze/util/inline.hpp"

Expand Down Expand Up @@ -105,6 +107,18 @@ namespace glz

inline constexpr std::array<uint8_t, 8> byte_count_lookup{1, 2, 4, 8, 16, 32, 64, 128};

// Types that BEVE encodes as elements of a numeric typed array.
//
// std::byte is serialized identically to a uint8_t (an unsigned 8-bit value): the
// numeric formulas below produce byte_count == 0, an unsigned type, and a u8 typed-array
// header for it, exactly matching uint8_t. It is the only byte_like type that is not
// already num_t (uint8_t and unsigned char are integral), so the numeric typed-array
// code paths must opt it in explicitly. This keeps an array of std::byte compact (a
// typed u8 array) and consistent with how a scalar std::byte is encoded (a u8 number),
// instead of an inflated generic array that stores a type header per element.
template <class T>
concept beve_num_t = num_t<T> || std::same_as<std::remove_cvref_t<T>, std::byte>;

[[nodiscard]] GLZ_ALWAYS_INLINE constexpr size_t int_from_compressed(auto&& ctx, auto&& it, auto end) noexcept
{
if (it >= end) [[unlikely]] {
Expand Down
61 changes: 51 additions & 10 deletions include/glaze/beve/read.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,21 @@ namespace glz
requires(std::is_enum_v<T> && !glaze_enum_t<T>)
struct from<BEVE, T>
{
// Tagged overload: the type tag has already been read and is supplied by the caller
// (used by the typed-array conversion paths). Delegate to the underlying integer's
// reader so enums reuse the same numeric decoding and conversion handling, then cast
// back. This makes std::byte (a byte-valued enum) readable as a u8 typed-array element.
template <auto Opts, class Value, class Tag, is_context Ctx, class It0, class It1>
requires(check_no_header(Opts))
GLZ_ALWAYS_INLINE static void op(Value&& value, Tag&& tag, Ctx&& ctx, It0&& it, It1 end) noexcept
{
using U = std::underlying_type_t<std::decay_t<T>>;
U underlying{};
from<BEVE, U>::template op<Opts>(underlying, std::forward<Tag>(tag), std::forward<Ctx>(ctx),
std::forward<It0>(it), end);
value = static_cast<std::decay_t<T>>(underlying);
}

template <auto Opts>
GLZ_ALWAYS_INLINE static void op(auto&& value, is_context auto&& ctx, auto&& it, auto end) noexcept
{
Expand Down Expand Up @@ -567,6 +582,33 @@ namespace glz
using V = typename std::decay_t<T>::value_type;
static_assert(sizeof(V) == 1);

// Stores n decoded bytes (already length-validated by the caller) into the target.
// Handles resizable strings, string views, and fixed-size std::array<char, N> uniformly
// so the tagged and untagged code paths share identical storage semantics.
GLZ_ALWAYS_INLINE static void store(auto&& value, is_context auto&& ctx, auto&& it, const size_t n)
{
if constexpr (string_view_t<T>) {
value = {it, n};
}
else if constexpr (array_char_t<T>) {
// Fixed-size std::array<char, N> cannot be resized, so the decoded payload must
// fit within it. Any trailing bytes are zero-filled to keep the buffer
// deterministic when a shorter payload is read into a larger array.
if (n > value.size()) [[unlikely]] {
ctx.error = error_code::syntax_error;
return;
}
std::memcpy(value.data(), it, n);
if (n < value.size()) {
std::memset(value.data() + n, 0, value.size() - n);
}
}
else {
value.resize(n);
std::memcpy(value.data(), it, n);
}
}

template <auto Opts>
requires(check_no_header(Opts))
GLZ_ALWAYS_INLINE static void op(auto&& value, const uint8_t, is_context auto&& ctx, auto&& it, auto end)
Expand All @@ -591,8 +633,10 @@ namespace glz
return;
}
}
value.resize(n);
std::memcpy(value.data(), it, n);
store(value, ctx, it, n);
if (bool(ctx.error)) [[unlikely]] {
return;
}
it += n;
}

Expand Down Expand Up @@ -633,12 +677,9 @@ namespace glz
}
}

if constexpr (string_view_t<T>) {
value = {it, n};
}
else {
value.resize(n);
std::memcpy(value.data(), it, n);
store(value, ctx, it, n);
if (bool(ctx.error)) [[unlikely]] {
return;
}
it += n;
}
Expand Down Expand Up @@ -689,7 +730,7 @@ namespace glz
}
}
}
else if constexpr (num_t<V>) {
else if constexpr (beve_num_t<V>) {
constexpr uint8_t type = std::floating_point<V> ? 0 : (std::is_signed_v<V> ? 0b000'01'000 : 0b000'10'000);
constexpr uint8_t header = tag::typed_array | type | (byte_count<V> << 5);

Expand Down Expand Up @@ -970,7 +1011,7 @@ namespace glz
}
}
}
else if constexpr (num_t<V>) {
else if constexpr (beve_num_t<V>) {
constexpr uint8_t type = std::floating_point<V> ? 0 : (std::is_signed_v<V> ? 0b000'01'000 : 0b000'10'000);
constexpr uint8_t header = tag::typed_array | type | (byte_count<V> << 5);

Expand Down
2 changes: 1 addition & 1 deletion include/glaze/beve/size.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ namespace glz
const auto num_bytes = (value.size() + 7) / 8;
result += num_bytes;
}
else if constexpr (num_t<V>) {
else if constexpr (beve_num_t<V>) {
if constexpr (check_aligned_arrays(Opts) && sizeof(V) > 1) {
result += 1; // extra numeric header byte
result += 1; // padding length byte
Expand Down
2 changes: 1 addition & 1 deletion include/glaze/beve/write.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ namespace glz
static_assert(false_v<T>);
}
}
else if constexpr (num_t<V>) {
else if constexpr (beve_num_t<V>) {
constexpr uint8_t type = std::floating_point<V> ? 0 : (std::is_signed_v<V> ? 0b000'01'000 : 0b000'10'000);
constexpr uint8_t numeric_header = tag::typed_array | type | (byte_count<V> << 5);

Expand Down
140 changes: 140 additions & 0 deletions tests/beve_test/beve_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6855,6 +6855,146 @@ suite beve_fixed_array_bounds_tests = [] {
};
};

// Regression coverage for https://github.com/stephenberry/glaze/issues/2647
// std::byte ranges must serialize as a compact u8 typed array (identical to uint8_t),
// not an inflated generic array, and fixed std::array<char, N> must round trip.
struct byte_packet_t
{
std::vector<std::byte> payload{};
int id{};
};

suite beve_byte_and_char_array_tests = [] {
"std::vector<std::byte> uses compact u8 typed array"_test = [] {
std::vector<std::byte> src{std::byte{0}, std::byte{1}, std::byte{0x7f}, std::byte{0xff}};
std::string byte_buffer{};
expect(not glz::write_beve(src, byte_buffer));

// Wire format must be byte-for-byte identical to the equivalent uint8_t vector.
std::vector<uint8_t> u8{0, 1, 0x7f, 0xff};
std::string u8_buffer{};
expect(not glz::write_beve(u8, u8_buffer));
expect(byte_buffer == u8_buffer);

// Compact: tag + compressed size + N data bytes (no per-element headers).
expect(byte_buffer.size() == 2 + src.size());

std::vector<std::byte> dst{};
expect(not glz::read_beve(dst, byte_buffer));
expect(dst == src);
};

"std::byte and uint8_t are cross-readable"_test = [] {
std::vector<std::byte> src{std::byte{10}, std::byte{20}, std::byte{30}};
std::string buffer{};
expect(not glz::write_beve(src, buffer));

std::vector<uint8_t> as_u8{};
expect(not glz::read_beve(as_u8, buffer));
expect(as_u8 == (std::vector<uint8_t>{10, 20, 30}));

std::vector<uint8_t> u8{1, 2, 3};
std::string u8_buffer{};
expect(not glz::write_beve(u8, u8_buffer));
std::vector<std::byte> as_byte{};
expect(not glz::read_beve(as_byte, u8_buffer));
expect(as_byte == (std::vector<std::byte>{std::byte{1}, std::byte{2}, std::byte{3}}));
};

"std::array<std::byte, N> round trips"_test = [] {
std::array<std::byte, 4> src{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}};
std::string buffer{};
expect(not glz::write_beve(src, buffer));
expect(buffer.size() == 2 + src.size());
std::array<std::byte, 4> dst{};
expect(not glz::read_beve(dst, buffer));
expect(dst == src);
};

"empty std::vector<std::byte> round trips"_test = [] {
std::vector<std::byte> src{};
std::string buffer{};
expect(not glz::write_beve(src, buffer));
std::vector<std::byte> dst{std::byte{9}};
expect(not glz::read_beve(dst, buffer));
expect(dst.empty());
};

"std::deque<std::byte> (non-contiguous) round trips"_test = [] {
std::deque<std::byte> src{std::byte{7}, std::byte{8}, std::byte{9}};
std::string buffer{};
expect(not glz::write_beve(src, buffer));
std::deque<std::byte> dst{};
expect(not glz::read_beve(dst, buffer));
expect(dst == src);
};

"std::byte vector renders to JSON as numbers"_test = [] {
std::vector<std::byte> src{std::byte{1}, std::byte{200}};
std::string buffer{};
expect(not glz::write_beve(src, buffer));
std::string json{};
expect(not glz::beve_to_json(buffer, json));
expect(json == "[1,200]");
};

"std::byte members inside a reflected struct"_test = [] {
byte_packet_t src{};
src.payload = {std::byte{0xDE}, std::byte{0xAD}, std::byte{0xBE}, std::byte{0xEF}};
src.id = 42;
std::string buffer{};
expect(not glz::write_beve(src, buffer));
byte_packet_t dst{};
expect(not glz::read_beve(dst, buffer));
expect(dst.payload == src.payload);
expect(dst.id == src.id);
};

"std::array<char, N> round trips (full)"_test = [] {
std::array<char, 16> src{};
const std::string_view text = "hello world";
std::memcpy(src.data(), text.data(), text.size());
std::string buffer{};
expect(not glz::write_beve(src, buffer));
std::array<char, 16> dst{};
for (auto& c : dst) c = 'x';
expect(not glz::read_beve(dst, buffer));
expect(dst == src);
};

"std::array<char, N> zero-fills when payload is shorter"_test = [] {
std::string src = "abc";
std::string buffer{};
expect(not glz::write_beve(src, buffer));
std::array<char, 8> dst{};
for (auto& c : dst) c = 'Z';
expect(not glz::read_beve(dst, buffer));
expect(std::string_view(dst.data(), 3) == "abc");
for (size_t i = 3; i < dst.size(); ++i) {
expect(dst[i] == '\0');
}
};

"std::array<char, N> rejects an oversized payload"_test = [] {
std::string src = "0123456789";
std::string buffer{};
expect(not glz::write_beve(src, buffer));
std::array<char, 4> dst{};
expect(bool(glz::read_beve(dst, buffer)));
};

"untagged std::byte and char array round trip"_test = [] {
std::tuple<std::vector<std::byte>, std::array<char, 8>> src{{std::byte{5}, std::byte{6}}, {}};
std::memcpy(std::get<1>(src).data(), "hi", 2);
std::string buffer{};
expect(not glz::write_beve_untagged(src, buffer));
std::tuple<std::vector<std::byte>, std::array<char, 8>> dst{};
expect(not glz::read_beve_untagged(dst, buffer));
expect(std::get<0>(dst) == std::get<0>(src));
expect(std::get<1>(dst) == std::get<1>(src));
};
};

int main()
{
trace.begin("binary_test");
Expand Down
Loading