diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6bbac0c..a34be5f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Added - Operations such as ``-=`` and ``++`` for store variables in C++. - YAML export of store meta-data. +- ``stored::MuxLayer`` to multiplex multiple protocol layers over a single connection. Changed ``````` @@ -34,6 +35,11 @@ Changed on ``keepAlive()``. - Improve reconnection behavior on protocol layers. +Fixed +````` + +- Init value of CRC32 in Python ``libstored.protocol.Crc32Layer``. + .. _Unreleased: https://github.com/DEMCON/libstored/compare/v2.0.0...HEAD diff --git a/examples/lossy_sync/main.cpp b/examples/lossy_sync/main.cpp index 4062bdbc..119039ef 100644 --- a/examples/lossy_sync/main.cpp +++ b/examples/lossy_sync/main.cpp @@ -22,11 +22,27 @@ #include enum { + // Interval in between polling of the sockets. PollInterval_ms = 100, + + // Interval to check if the Synchronizers need to send out updates to the other party. SyncInterval_ms = PollInterval_ms * 5, - IdleTimeout_ms = SyncInterval_ms, - DisconnectTimeout_ms = IdleTimeout_ms * 10, + + // Interval to retransmit unacknowledged packets over the lossy line. + // Do this more often than SyncInterval_ms to avoid delays. + RetransmitInterval_ms = PollInterval_ms * 3, + + // Timeout value to send out a keep-alive packet. + // In this application, this should not be needed, as synchronization is faster. + IdleTimeout_ms = SyncInterval_ms * 2, + + // Timeout until we give up on the connection. + DisconnectTimeout_ms = IdleTimeout_ms * 5, + + // Update the heartbeat value in the store. HeartbeatInterval_ms = 1000, + + // Delay before trying to reconnect after a disconnection. ReconnectDelay_ms = DisconnectTimeout_ms + IdleTimeout_ms * 2, }; @@ -283,6 +299,13 @@ class SyncStack { if(verbose) wrap(stdout, "sync"); + // Add another channel to send pings. + auto mux = wrap(); + auto ch1_print = alloc(stdout, "chan"); + m_ch1 = alloc(); + m_ch1->wrap(*ch1_print); + mux.get()->map(1, *m_ch1); + // We don't want to do ARQ on large messages, so we segment them to some // appropriate size. wrap(32U); @@ -319,24 +342,25 @@ class SyncStack { wrap(stdout, "raw"); // Connect to I/O. - m_zmqLayer.wrap(*m_layers.back()); + m_zmqLayer.wrap(*m_stack.back()); // Register the store... m_synchronizer.map(store); // ...and the protocol stack. - m_synchronizer.connect(**m_layers.begin()); + m_synchronizer.connect(**m_stack.begin()); // There we go! auto now = std::chrono::steady_clock::now(); m_idleUpSince = now; m_idleDownSince = now; + m_lastRetransmit = now; m_lastSync = now; m_lastHeartbeat = now; m_heartbeat = server ? store.server_heartbeat.variable() : store.client_heartbeat.variable(); if(!server) { - m_synchronizer.syncFrom(store, *m_layers.front()); + m_synchronizer.syncFrom(store, *m_stack.front()); m_connected = true; m_arq->keepAlive(); } @@ -354,6 +378,7 @@ class SyncStack { recv(); doSync(now); checkRetransmit(now); + checkIdle(now); checkDisconnect(now); doHeartbeat(now); @@ -366,16 +391,25 @@ class SyncStack { } protected: + template + std::shared_ptr alloc(Args&&... args) + { + auto* p = new T{std::forward(args)...}; + std::shared_ptr layer{p}; + m_layers.emplace_back(layer); + return layer; + } + template std::shared_ptr wrap(Args&&... args) { auto* p = new T{std::forward(args)...}; std::shared_ptr layer{p}; - if(!m_layers.empty()) - layer->wrap(*m_layers.back()); + if(!m_stack.empty()) + layer->wrap(*m_stack.back()); - m_layers.emplace_back(layer); + m_stack.emplace_back(layer); return layer; } @@ -404,6 +438,26 @@ class SyncStack { void checkRetransmit(std::chrono::time_point const& now) { + // Check if we need to retransmit messages that have not been acked yet. + + if(!connected()) + return; + + if(m_idle->idleDown()) { + auto dt = now - m_lastRetransmit; + if(dt > std::chrono::milliseconds(RetransmitInterval_ms)) { + m_arq->process(); + m_lastRetransmit = now; + } + } else { + m_lastRetransmit = now; + } + } + + void checkIdle(std::chrono::time_point const& now) + { + // Check if we need to send out a keep-alive message once in a while. + if(!connected()) return; @@ -442,7 +496,13 @@ class SyncStack { auto dt = now - m_lastHeartbeat; if(dt >= std::chrono::milliseconds(HeartbeatInterval_ms)) { m_lastHeartbeat = now; - m_heartbeat++; + auto h = m_heartbeat++; + + if(connected()) { + char buf[32]; + snprintf(buf, sizeof(buf), "ping %u", h); + m_ch1->encode(buf, strlen(buf), true); + } } } @@ -467,11 +527,14 @@ class SyncStack { stored::Synchronizer m_synchronizer; std::shared_ptr m_arq; std::shared_ptr m_idle; + std::shared_ptr m_ch1; std::list> m_layers; + std::list> m_stack; stored::ZmqLayer m_zmqLayer; stored::PollableZmqSocket m_pollable{m_zmqLayer.socket(), stored::Pollable::PollIn}; std::chrono::time_point m_idleUpSince; std::chrono::time_point m_idleDownSince; + std::chrono::time_point m_lastRetransmit; std::chrono::time_point m_lastSync; std::chrono::time_point m_lastHeartbeat; stored::Variable m_heartbeat; diff --git a/include/libstored/protocol.h b/include/libstored/protocol.h index f50b515d..354d912d 100644 --- a/include/libstored/protocol.h +++ b/include/libstored/protocol.h @@ -610,6 +610,7 @@ class ArqLayer : public ProtocolLayer { virtual void disconnected() override; bool isConnected() const; void keepAlive(); + int process(); enum Event { /*! @@ -1361,6 +1362,110 @@ make_callback(Up&& up, Down&& down, Connected&& connected, Disconnected&& discon } # endif // C++11 +/*! + * \brief A that multiplexes between protocol stacks. + * + * Channel data is prefixed by a one-byte channel identifier. These channels can be split by the + * receiver to separate stacks. Channel ID 0 is used for the stack above the MuxLayer itself. + * + * One can use this layer to multiplex different protocols or logging output over the same physical + * channel. In case a lossless channel is used, the following stack could be used: + * + * channel 0: + * - Debugger + * - SegmentationLayer + * - AsciiEscapeLayer + * - TerminalLayer + * + * channel 1: + * - PrintLayer + * + * - MuxLayer + * - some lossless transport layer + * + * In this case, the MuxLayer passes through a stream of bytes, so framing is required in the + * channel 0 stack. The bytes of channel 1 are just printed for logging. + * + * When having a lossy channel, add an ArqLayer below the MuxLayer. + * + * channel 0 + * - Debugger + * + * channel 1 + * - PrintLayer + * + * - MuxLayer + * - SegmentationLayer + * - Crc32Layer + * - ArqLayer + * - AsciiEscapeLayer + * - TerminalLayer + * - some lossy transport layer + * + * Now, the framing is done below the ArqLayer, so all \c decode()s get full frames above the + * MuxLayer. Therefore, no framing is required in channel 0. + */ +class MuxLayer : public ProtocolLayer { + STORED_CLASS_NOCOPY(MuxLayer) +public: + typedef ProtocolLayer base; + typedef uint8_t ChannelId; + + static char const Esc = '\x10'; // DLE + static char const Repeat = '\x15'; // NAK + + explicit MuxLayer(ProtocolLayer* up = nullptr, ProtocolLayer* down = nullptr); + virtual ~MuxLayer() override; + +# if STORED_cplusplus >= 201103L + MuxLayer(std::initializer_list> layers); + void map(std::initializer_list> layers); +# endif // C++11 + + void map(ChannelId channel, ProtocolLayer& layer); + void unmap(ChannelId channel); + void unmap(); + + virtual void decode(void* buffer, size_t len) override; + virtual void encode(void const* buffer, size_t len, bool last = true) override; +# ifndef DOXYGEN + using base::encode; +# endif + virtual void reset() override; + virtual void connected() override; + virtual void disconnected() override; + virtual size_t mtu() const override; + +protected: + void decode_(void* buffer, size_t len); + void encode_(ChannelId channel, void const* buffer, size_t len, bool last = true); + + class Channel final : public ProtocolLayer { + STORED_CLASS_NOCOPY(Channel) + public: + typedef ProtocolLayer base; + Channel(MuxLayer& mux, ChannelId channel, ProtocolLayer& up); + virtual ~Channel() override is_default + + virtual void encode(void const* buffer, size_t len, bool last = true) override; + virtual size_t mtu() const override; + + private: + MuxLayer* m_mux; + ChannelId m_channel; + }; + +private: + ssize_t channelIndex(ChannelId id) const; + ProtocolLayer* channel(ChannelId id); + +private: + Vector::type m_channels; + ChannelId m_encodingChannel; + ProtocolLayer* m_decodingChannel; + bool m_decodingEsc; +}; + namespace impl { class Loopback1 final : public ProtocolLayer { STORED_CLASS_NOCOPY(Loopback1) diff --git a/python/libstored/protocol/protocol.py b/python/libstored/protocol/protocol.py index c9ba5bb4..7c9270d3 100644 --- a/python/libstored/protocol/protocol.py +++ b/python/libstored/protocol/protocol.py @@ -820,7 +820,7 @@ class Crc32Layer(ProtocolLayer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._crc = crcmod.mkCrcFun(0x104c11db7, 0xffffffff, True, 0xffffffff) + self._crc = crcmod.mkCrcFun(0x104c11db7, 0, True, 0xffffffff) async def encode(self, data: ProtocolLayer.Packet) -> None: if isinstance(data, str): diff --git a/sphinx/doc/cpp_protocol.rst b/sphinx/doc/cpp_protocol.rst index bff61434..983917f7 100644 --- a/sphinx/doc/cpp_protocol.rst +++ b/sphinx/doc/cpp_protocol.rst @@ -93,6 +93,7 @@ The inheritance of the layers is shown below. ProtocolLayer <|-- PrintLayer ProtocolLayer <|-- IdleCheckLayer ProtocolLayer <|-- CallbackLayer + ProtocolLayer <|-- MuxLayer abstract ArqLayer SegmentationLayer -[hidden]--> ArqLayer @@ -208,6 +209,11 @@ stored::Loopback .. doxygenclass:: stored::Loopback +stored::MuxLayer +---------------- + +.. doxygenclass:: stored::MuxLayer + stored::NamedPipeLayer ---------------------- diff --git a/src/protocol.cpp b/src/protocol.cpp index b4d89ec8..cea4c143 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -835,13 +835,33 @@ bool ArqLayer::isConnected() const return m_connected; } +/*! + * \brief Process queued messages. + * + * Call this function at a regular interval to retransmit messages, when necessary. + * When no messages are queued, this function does nothing. + * + * To send out keep-alive messages, use #keepAlive(). A common pattern would be to call this + * function relatively often (e.g., every 100 ms), and #keepAlive() less often (e.g., every 1 + * second). + * + * \return EAGAIN when there was nothing to process, 0 when something was sent out, or an \c errno + * otherwise. + */ +int ArqLayer::process() +{ + return transmit() ? 0 : EAGAIN; +} + /*! * \brief Send a keep-alive packet to check the connection. * - * It actually retransmits the message that is currently processed (waiting for - * an ack), or sends a dummy message in case the encode queue is empty. Either - * way, #retransmits() and the #EventRetransmit can be used afterwards to - * determine the quality of the link. + * It actually retransmits the message that is currently processed (waiting for an ack), or sends a + * dummy message in case the encode queue is empty. Either way, #retransmits() and the + * #EventRetransmit can be used afterwards to determine the quality of the link. + * + * If you want just only retransmits without sending a keep-alive when there is nothing to send, use + * #process() instead. */ void ArqLayer::keepAlive() { @@ -1951,6 +1971,314 @@ void LossyLayer::ber(float ber) +////////////////////////////// +// MuxLayer +// + +/*! + * \brief Ctor. + */ +MuxLayer::MuxLayer(ProtocolLayer* up, ProtocolLayer* down) + : base(up, down) + , m_encodingChannel(Repeat) + , m_decodingChannel() + , m_decodingEsc() +{} + +/*! + * \brief Dtor. + */ +MuxLayer::~MuxLayer() +{ + unmap(); +} + +#if STORED_cplusplus >= 201103L +/*! + * \brief Ctor with full mapping. + */ +MuxLayer::MuxLayer(std::initializer_list> layers) + : MuxLayer() +{ + map(layers); +} + +/*! + * \brief Map all channels at once. + */ +void MuxLayer::map(std::initializer_list> layers) +{ + unmap(); + + ChannelId c = 0; + for(auto const& l : layers) { + map(c++, l.get()); // Channel 0 for all layers. + } +} +#endif // C++11 + +/*! + * \brief Returns the Channel index in #m_channels, or -1 when invalid. + */ +ssize_t MuxLayer::channelIndex(ChannelId id) const +{ + static_assert(Esc < Repeat, ""); + + if(id == 0) + return -1; + if(id < Esc) + return (ssize_t)(id - 1); + if(id == Esc) + return -1; + if(id < Repeat) + return (ssize_t)(id - 2); + if(id == Repeat) + return -1; + + return (ssize_t)(id - 3); +} + +/*! + * \brief Returns the protocol stack for the given channel. + */ +ProtocolLayer* MuxLayer::channel(ChannelId id) +{ + if(id == 0) + return this; + + ssize_t i = channelIndex(id); + if(i < 0 || (size_t)i >= m_channels.size()) + return nullptr; + + return m_channels[(size_t)i]; +} + +/*! + * \brief Map a channel ID to the given protocol stack. + */ +void MuxLayer::map(ChannelId channel, ProtocolLayer& layer) +{ + stored_assert(channel != Esc && channel != Repeat); + + if(channel == 0) { + wrap(layer); + return; + } + + ssize_t i = channelIndex(channel); + stored_assert(i >= 0); + + unmap(channel); + + if((size_t)i >= m_channels.size()) + m_channels.resize((size_t)i + 1, nullptr); + + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + Channel* c = new Channel(*this, channel, layer); + m_channels[(size_t)i] = c; +} + +/*! + * \brief Remove a channel mapping. + */ +void MuxLayer::unmap(ChannelId channel) +{ + if(channel == 0) { + if(up() && up()->down() == this) + up()->setDown(); + + setUp(nullptr); + return; + } + + ssize_t i = channelIndex(channel); + if(i < 0 || (size_t)i >= m_channels.size()) + return; + + Channel* c = m_channels[(size_t)i]; + if(!c) + return; + + m_channels[(size_t)i] = nullptr; + c->disconnected(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete c; +} + +/*! + * \brief Unmap all channels. + */ +void MuxLayer::unmap() +{ + for(ChannelId c = 0; (size_t)c < m_channels.size() + 2; c++) + unmap(c); +} + +void MuxLayer::encode(void const* buffer, size_t len, bool last) +{ + encode_(0, buffer, len, last); +} + +void MuxLayer::reset() +{ + m_encodingChannel = Repeat; + m_decodingChannel = nullptr; + base::reset(); +} + +void MuxLayer::connected() +{ + m_encodingChannel = Repeat; + base::connected(); + + for(size_t i = 0; i < m_channels.size(); i++) { + Channel* c = m_channels[i]; + if(c) + c->connected(); + } +} + +void MuxLayer::disconnected() +{ + m_encodingChannel = Repeat; + m_decodingChannel = nullptr; + base::disconnected(); + + for(size_t i = 0; i < m_channels.size(); i++) { + Channel* c = m_channels[i]; + if(c) + c->disconnected(); + } +} + +void MuxLayer::decode(void* buffer, size_t len) +{ + uint8_t* buffer_ = static_cast(buffer); + size_t out_start = 0; + size_t out_end = 0; + size_t in = 0; + + while(in < len) { + uint8_t b = buffer_[in++]; + + if(unlikely(m_decodingEsc)) { + m_decodingEsc = false; + if(b == Esc) { + // Escaped escape byte. + buffer_[out_end++] = Esc; + } else if(b == Repeat) { + // Just a control command in between. + // Repeat channel id on next encode. + m_encodingChannel = Repeat; + } else { + // Switch channel. + decode_(buffer_ + out_start, out_end - out_start); + out_start = out_end = in; + m_decodingChannel = channel(b); + } + } else { + if(unlikely(b == Esc)) { + // Escape byte. + m_decodingEsc = true; + } else { + // Normal byte. + size_t i = out_end++; + if(unlikely(out_end != in)) + buffer_[i] = b; + } + } + } + + if(likely(out_end > out_start)) + decode_(buffer_ + out_start, out_end - out_start); +} + +/*! + * \brief Forward decoded data. + */ +void MuxLayer::decode_(void* buffer, size_t len) +{ + if(!buffer || len == 0) + return; + + if(m_decodingChannel == this) + base::decode(buffer, len); + else if(m_decodingChannel) + m_decodingChannel->decode(buffer, len); +} + +/*! + * \brief Encode data for a given channel. + */ +void MuxLayer::encode_(ChannelId channel, void const* buffer, size_t len, bool last) +{ + if(channel == Repeat) + return; + + if(channel != m_encodingChannel) { + uint8_t buf[2] = {Esc, channel}; + base::encode(buf, sizeof(buf), false); + m_encodingChannel = channel; + } + + char const* buffer_ = static_cast(buffer); + size_t i = 0; + bool enc_last = false; + + while(i < len) { + // Find next escape byte. + size_t c = i; + for(; c < len; c++) { + if(buffer_[c] == Esc) + break; + } + + if(c == len) + // No escapes in the rest of the buffer. + break; + + // Found an escape byte at position c. Repeat escape byte. + base::encode(buffer_ + i, c - i + 1, false); + enc_last = c == len && last; + base::encode(buffer_ + c, 1, enc_last); + i = c + 1; + } + + if(!enc_last && (i < len || last)) + base::encode(buffer_ + i, len - i, last); +} + +size_t MuxLayer::mtu() const +{ + size_t mtu = base::mtu(); + if(mtu == 0U) + return 0U; + if(mtu <= 2U) + return 1U; + return std::max(1U, (mtu - 2U) / 2U); +} + +MuxLayer::Channel::Channel(MuxLayer& mux, ChannelId channel, ProtocolLayer& up) + : m_mux(&mux) + , m_channel(channel) +{ + wrap(up); +} + +void MuxLayer::Channel::encode(void const* buffer, size_t len, bool last) +{ + stored_assert(m_mux); + m_mux->encode_(m_channel, buffer, len, last); +} + +size_t MuxLayer::Channel::mtu() const +{ + stored_assert(m_mux); + return m_mux->mtu(); +} + + + ////////////////////////////// // Loopback // diff --git a/tests/test_protocol.cpp b/tests/test_protocol.cpp index 9f1b6aa8..efb7e793 100644 --- a/tests/test_protocol.cpp +++ b/tests/test_protocol.cpp @@ -795,6 +795,82 @@ TEST(Crc16Layer, Decode) EXPECT_EQ(ll.decoded().size(), 0); } +TEST(Crc32Layer, Encode) +{ + stored::Crc32Layer l; + LoggingLayer ll; + ll.wrap(l); + + ll.encoded().clear(); + l.encode(); + EXPECT_EQ(ll.encoded().size(), 1); + EXPECT_EQ(ll.encoded().at(0)[0], '\x00'); + EXPECT_EQ(ll.encoded().at(0)[1], '\x00'); + EXPECT_EQ(ll.encoded().at(0)[2], '\x00'); + EXPECT_EQ(ll.encoded().at(0)[3], '\x00'); + + ll.encoded().clear(); + l.encode("1", 1); + EXPECT_EQ(ll.encoded().size(), 1); + EXPECT_EQ(ll.encoded().at(0), "1\x83\xDC\xEF\xB7"); + + ll.encoded().clear(); + l.encode("12", 2); + EXPECT_EQ(ll.encoded().size(), 1); + EXPECT_EQ(ll.encoded().at(0), "12OSD\xCD"); + + ll.encoded().clear(); + l.encode("123", 3); + EXPECT_EQ(ll.encoded().size(), 1); + EXPECT_EQ(ll.encoded().at(0), "123\x88Hc\xD2"); + + ll.encoded().clear(); + l.encode("1234", 4); + EXPECT_EQ(ll.encoded().size(), 1); + EXPECT_EQ(ll.encoded().at(0), "1234\x9B\xE3\xE0\xA3"); +} + +TEST(Crc32Layer, Decode) +{ + LoggingLayer ll; + stored::Crc32Layer l; + l.wrap(ll); + + ll.decoded().clear(); + DECODE(l, "\0\0\0\0"); + EXPECT_EQ(ll.decoded().size(), 1); + EXPECT_EQ(ll.decoded().at(0), ""); + + ll.decoded().clear(); + DECODE(l, "1\x83\xDC\xEF\xB7"); + EXPECT_EQ(ll.decoded().size(), 1); + EXPECT_EQ(ll.decoded().at(0), "1"); + + ll.decoded().clear(); + DECODE(l, "12OSD\xCD"); + EXPECT_EQ(ll.decoded().size(), 1); + EXPECT_EQ(ll.decoded().at(0), "12"); + + ll.decoded().clear(); + DECODE(l, "123\x88Hc\xD2"); + EXPECT_EQ(ll.decoded().size(), 1); + EXPECT_EQ(ll.decoded().at(0), "123"); + + ll.decoded().clear(); + DECODE(l, "1234\x9B\xE3\xE0\xA3"); + EXPECT_EQ(ll.decoded().size(), 1); + + ll.decoded().clear(); + DECODE(l, "123\x9B\xE3\xE0\xA3"); + EXPECT_EQ(ll.decoded().size(), 0); + + ll.decoded().clear(); + DECODE(l, + "\x00" + "1234\x9B\xE3\xE0\xA3"); + EXPECT_EQ(ll.decoded().size(), 0); +} + TEST(BufferLayer, Encode) { stored::BufferLayer l(4); @@ -1368,4 +1444,66 @@ TEST(TerminalLayer, Decode) EXPECT_EQ(ll.decoded().at(0), "flowers"); } +TEST(MuxLayer, Encode) +{ + LoggingLayer ch0; + LoggingLayer ch1; + LoggingLayer ch2; + + stored::MuxLayer l{{ch0, ch1, ch2}}; + LoggingLayer ll; + ll.wrap(l); + + ch0.encode(" ch0", 4); + EXPECT_EQ(ll.encoded().at(0), std::string("\x10\x00 ch0", 6)); + + ch0.encode("abc", 3); + EXPECT_EQ(ll.encoded().at(1), "abc"); + + ch1.encode(" ch1", 4); + EXPECT_EQ(ll.encoded().at(2), "\x10\x01 ch1"); + + ch2.encode(" ch\x10 2", 6); + EXPECT_EQ(ll.encoded().at(3), "\x10\x02 ch\x10\x10 2"); +} + +TEST(MuxLayer, Decode) +{ + LoggingLayer ch0; + LoggingLayer ch1; + LoggingLayer ch2; + + stored::MuxLayer l{{ch0, ch1, ch2}}; + + DECODE(l, "ch?"); + EXPECT_EQ(ch0.decoded().size(), 0); + + DECODE(l, "\x10\x00 ch0"); + EXPECT_EQ(ch0.decoded().at(0), " ch0"); + + DECODE(l, "ch0"); + EXPECT_EQ(ch0.decoded().at(1), "ch0"); + + DECODE(l, "\x10\x01 ch1"); + EXPECT_EQ(ch1.decoded().at(0), " ch1"); + + DECODE(l, "\x10\x02 ch2"); + EXPECT_EQ(ch2.decoded().at(0), " ch2"); + + DECODE(l, "ch\x10\x10 2"); + EXPECT_EQ(ch2.decoded().at(1), "ch\x10 2"); + + DECODE(l, "\x10\x01 1\x10"); + EXPECT_EQ(ch1.decoded().at(1), " 1"); + DECODE(l, "\x02 2"); + EXPECT_EQ(ch2.decoded().at(2), " 2"); + + DECODE(l, "\x10\x00 0\x10\x15 1"); + EXPECT_EQ(ch0.decoded().at(2), " 0 1"); + + DECODE(l, "\x10\x03 ch?"); + EXPECT_EQ(ch0.decoded().size(), 3); +} + + } // namespace