xo-umbrella2/xo-ppsink/include/xo/ppsink/hex.hpp

266 lines
9.7 KiB
C++

/** @file hex.hpp
*
* @author Roland Conybeare, Aug 2026
*
* hex_view(range, style): print a range of bytes in hexadecimal. The
* ppsink-native replacement for legacy xo-indentlog xo::hex_view
* (xo/indentlog/print/hex.hpp).
*
* @code
* sink.pp(hex_view(s)); // [68 65 6c 6c 6f]
* sink.pp(hex_view(s, hexstyle::with_char)); // [68(h) 65(e) 6c(l) ..]
* @endcode
*
* Printable-character detection uses an explicit ASCII range test rather
* than std::isprint, so output does not vary with the active locale. The
* two agree in the "C" locale.
*
* This header is ostream-free; see hex_ostream.hpp to stream a hex_view
* directly to an ostream. (Using one as a tag value -- xtag("k", hex_view(s))
* -- needs only this header, since the tag machinery renders it through the
* sink.)
**/
#pragma once
#include "Prettifier.hpp"
#include <ranges>
#include <cstddef>
#include <cstdint>
#include <string_view>
namespace xo::pp {
/** @brief whether to annotate each byte with its ascii character **/
enum class hexstyle {
/** just the hex digits: @c [68 65 6c] **/
bare,
/** hex digits plus the character: @c [68(h) 65(e) 6c(l)] **/
with_char,
};
/** @brief whether to qualify a byte with the @c 0x radix prefix. **/
enum class hexprefix {
/** just the digits: @c fd **/
plain,
/** 0x-qualified: @c 0xfd **/
qualified,
};
namespace detail {
/** @brief a contiguous range of 1-byte elements.
*
* Structural, not nominal: xo::mm::span and xo::scm::span satisfy it
* without ppsink naming (or depending on) xo-arena or xo-tokenizer.
**/
template <typename R>
concept byte_range = (std::ranges::contiguous_range<R>
&& std::ranges::sized_range<R>
&& sizeof(std::ranges::range_value_t<R>) == 1);
} /*namespace detail*/
/** @brief a range (unowned) of bytes, to print in hexadecimal.
*
* Refers to memory it does not own, so the range must outlive the
* hex_view. Safe in a single print expression; bind to a named local
* first if you need it to live longer.
**/
class hex_view {
public:
/** number of bytes per output row. Rows are the only break
* opportunities, and match the row width a hexdump-style renderer
* would use.
**/
static constexpr std::size_t c_bytes_per_row = 16;
public:
/** print the byte range [@p lo, @p hi) **/
constexpr hex_view(const std::uint8_t * lo,
const std::uint8_t * hi,
hexstyle style = hexstyle::bare) noexcept
: lo_{lo}, hi_{hi}, style_{style} {}
/* the ctors below are deliberately not constexpr: reinterpret_cast is
* not allowed in a constant expression, so marking them constexpr
* would be a promise the compiler can never honour (-Winvalid-constexpr).
*/
/** print the char range [@p lo, @p hi) **/
hex_view(const char * lo,
const char * hi,
hexstyle style = hexstyle::bare) noexcept
: lo_{reinterpret_cast<const std::uint8_t *>(lo)},
hi_{reinterpret_cast<const std::uint8_t *>(hi)},
style_{style} {}
/** print the bytes of @p r.
*
* explicit: an implicit conversion from every byte range would make
* hex_view a candidate in unrelated overload sets.
**/
template <detail::byte_range R>
explicit hex_view(const R & r,
hexstyle style = hexstyle::bare)
: lo_{reinterpret_cast<const std::uint8_t *>(std::ranges::data(r))},
hi_{lo_ + std::ranges::size(r)},
style_{style} {}
constexpr const std::uint8_t * lo() const noexcept { return lo_; }
constexpr const std::uint8_t * hi() const noexcept { return hi_; }
constexpr hexstyle style() const noexcept { return style_; }
constexpr std::size_t size() const noexcept {
return static_cast<std::size_t>(hi_ - lo_);
}
private:
/** print bytes starting here **/
const std::uint8_t * lo_;
/** print bytes up to (not including) here **/
const std::uint8_t * hi_;
/** whether to annotate each byte with its character **/
hexstyle style_;
};
/** @brief a single byte, to print in hexadecimal.
*
* @code
* sink.pp(hex(0xfd)); // fd
* sink.pp(hex(0x4f, hexstyle::with_char)); // 4f(O)
* sink.pp(hex(0xfd, hexprefix::qualified)); // 0xfd
* @endcode
*
* Defaults to unprefixed, matching hex_view (which renders [68 65 6c],
* not [0x68 ..]) and legacy xo::hex. Pass hexprefix::qualified where
* the surrounding context does not already say "this is hex" -- a lone
* field among decimal ones, say.
*
* Unlike hex_view this owns its byte, so it has no lifetime constraint.
**/
class hex {
public:
/** print @p x in hexadecimal, annotated per @p style,
* qualified per @p prefix
**/
constexpr explicit hex(std::uint8_t x,
hexstyle style = hexstyle::bare,
hexprefix prefix = hexprefix::plain) noexcept
: x_{x}, style_{style}, prefix_{prefix} {}
/** ctor with prefix-argument first **/
constexpr explicit hex(std::uint8_t x,
hexprefix prefix,
hexstyle style = hexstyle::bare) noexcept
: x_{x}, style_{style}, prefix_{prefix} {}
constexpr std::uint8_t value() const noexcept { return x_; }
constexpr hexstyle style() const noexcept { return style_; }
constexpr hexprefix prefix() const noexcept { return prefix_; }
private:
/** the byte to print **/
std::uint8_t x_;
/** whether to annotate with the ascii character **/
hexstyle style_;
/** whether to qualify with the 0x radix prefix **/
hexprefix prefix_;
};
namespace detail {
/** lowercase nibble -> hex digit **/
inline constexpr char c_hex_digit[] = "0123456789abcdef";
/** true iff @p uc is a printable ascii character.
* Deliberately not std::isprint: locale-independent, so test
* expectations do not shift with the environment.
**/
inline constexpr bool
is_ascii_printable(std::uint8_t uc) noexcept {
return (uc >= 0x20) && (uc < 0x7f);
}
/** emit one byte as a single token: @c 6c , or @c 6c(l) with @p style,
* or @c 0x6c(l) with @p prefix
**/
inline void
put_hex_byte(PpSink & sink,
std::uint8_t uc,
hexstyle style,
hexprefix prefix = hexprefix::plain) {
/* "0xff(c)" is the widest form */
char buf[7];
std::size_t n = 0;
if (prefix == hexprefix::qualified) {
buf[n++] = '0';
buf[n++] = 'x';
}
buf[n++] = c_hex_digit[(uc >> 4) & 0x0f];
buf[n++] = c_hex_digit[uc & 0x0f];
if (style == hexstyle::with_char) {
buf[n++] = '(';
buf[n++] = (is_ascii_printable(uc)
? static_cast<char>(uc)
: '?');
buf[n++] = ')';
}
/* one put() per byte: a byte must never be split across lines */
sink.put(std::string_view(buf, n));
}
} /*namespace detail*/
/** @brief render a hex into a PpSink.
*
* One put(), no group: a byte is a single token and must never be split
* across lines. Shares detail::put_hex_byte() with Prettifier<hex_view>,
* so a byte renders identically alone or inside a dump.
**/
template <>
struct Prettifier<hex> {
static void print(PpSink & sink, const hex & x) {
detail::put_hex_byte(sink, x.value(), x.style(), x.prefix());
}
};
/** @brief render a hex_view into a PpSink.
*
* Shape notes -- these differ from pretty_struct.hpp on purpose:
*
* - put("[") *before* begin(), so the bracket stays on the opening line.
* Same rule as pretty_struct's put(name).
* - NO leading split after begin(). pretty_struct emits a split before
* every field including the first; here legacy renders "[68 .." with no
* space after the bracket, so a leading split would add one. Please
* don't "fix" this to match pretty_struct.
* - split(1) only at row boundaries; plain put(" ") between bytes within
* a row. Splits are break opportunities, so a split between every byte
* would let a line break mid-row.
* - put("]") *before* end(), so the closing bracket counts toward the
* group's width when the sink decides whether the group fits.
**/
template <>
struct Prettifier<hex_view> {
static void print(PpSink & sink, const hex_view & x) {
sink.put("[");
sink.begin();
std::size_t i = 0;
for (const std::uint8_t * p = x.lo(); p < x.hi(); ++p, ++i) {
if (i > 0) {
if (i % hex_view::c_bytes_per_row == 0)
sink.split(1);
else
sink.put(" ");
}
detail::put_hex_byte(sink, *p, x.style());
}
sink.put("]");
sink.end();
}
};
} /*namespace xo::pp*/
/* end hex.hpp */