blob: d0ab87e7f066e87e10bbf029b239da8825749b76 [file]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include <cstddef>
#include "pw_allocator/allocator.h"
#include "pw_allocator/testing.h"
#include "pw_assert/check.h"
#include "pw_async2/basic_dispatcher.h"
#include "pw_async2/channel.h"
#include "pw_async2/func_task.h"
#include "pw_async2/poll.h"
#include "pw_async2/task.h"
#include "pw_async2/try.h"
#include "pw_async2/waker.h"
#include "pw_bytes/endian.h"
#include "pw_bytes/span.h"
#include "pw_multibuf/examples/protocol.h"
#include "pw_multibuf/multibuf.h"
#include "pw_random/xor_shift.h"
#include "pw_span/cast.h"
#include "pw_unit_test/framework.h"
namespace examples {
struct DemoLinkHeader : pw::multibuf::examples::DemoLinkHeader {
static constexpr size_t kLength = pw::multibuf::examples::kDemoLinkHeaderLen;
};
struct DemoLinkFooter : pw::multibuf::examples::DemoLinkFooter {
static constexpr size_t kLength = pw::multibuf::examples::kDemoLinkFooterLen;
};
struct DemoNetworkHeader : pw::multibuf::examples::DemoNetworkHeader {
static constexpr size_t kLength =
pw::multibuf::examples::kDemoNetworkHeaderLen;
};
struct DemoTransportHeader : pw::multibuf::examples::DemoTransportHeader {
static constexpr size_t kLength =
pw::multibuf::examples::kDemoTransportHeaderLen;
};
constexpr size_t kMaxDemoLinkFrameLength =
pw::multibuf::examples::kMaxDemoLinkFrameLength;
// The maximum number of active link frame buffers.
constexpr size_t kCapacity = 4;
/// Gets a protocol field from a protocol header stored in a MultiBuf.
template <typename T>
constexpr T GetHeaderField(const pw::ConstMultiBuf& mbuf, size_t offset) {
pw::ConstByteSpan header = *(mbuf.ConstChunks().cbegin());
return pw::bytes::ReadInOrder<T>(pw::endian::little, &header[offset]);
}
/// Sets a protocol field for a protocol header stored in a MultiBuf.
template <typename T>
constexpr void SetHeaderField(pw::MultiBuf& mbuf, size_t offset, T value) {
pw::ByteSpan header = *(mbuf.Chunks().begin());
return pw::bytes::CopyInOrder<T>(pw::endian::little, value, &header[offset]);
}
// Forward decalrations.
class TransportSegment;
class NetworkPacket;
class LinkFrame {
public:
/// Creates a new link frame backed by the given `buffer`, and using the given
/// `allocator` to allocate MultiBuf metadata.
static pw::Result<LinkFrame> Create(pw::Allocator& allocator,
pw::ByteSpan buffer) {
pw::MultiBuf::Instance mbuf(allocator);
if (!mbuf->TryReserveLayers(4)) {
return pw::Status::ResourceExhausted();
}
mbuf->PushBack(buffer);
PW_CHECK(mbuf->AddLayer(0));
return LinkFrame(std::move(*mbuf));
}
/// Convert a network packet to its underlying link frame.
static LinkFrame From(NetworkPacket&& packet);
constexpr uint16_t length() const {
return GetHeaderField<uint16_t>(*mbuf_, offsetof(DemoLinkHeader, length));
}
/// Erases all data and sets the frame length back to the size of the buffer
/// that was provided in the constructor.
void Reset() {
mbuf_->PopLayer();
for (auto chunk : mbuf_->Chunks()) {
std::memset(chunk.data(), 0, chunk.size());
}
PW_CHECK(mbuf_->AddLayer(0));
ResetLength();
}
private:
friend class NetworkPacket;
constexpr explicit LinkFrame(pw::MultiBuf&& mbuf) : mbuf_(std::move(mbuf)) {
PW_CHECK_UINT_EQ(mbuf_->NumLayers(), 2u);
ResetLength();
}
/// Updates the link frame header to match the MultiBuf.
void ResetLength() {
size_t length = mbuf_->size();
PW_CHECK_UINT_LE(length, std::numeric_limits<uint16_t>::max());
SetHeaderField<uint16_t>(*mbuf_,
offsetof(DemoLinkHeader, length),
static_cast<uint16_t>(mbuf_->size()));
}
pw::MultiBuf::Instance mbuf_;
};
class NetworkPacket {
public:
/// Convert a network packet to the link frame it holds.
static NetworkPacket From(LinkFrame&& frame);
/// Convert a transport segment to its underlying network packet.
static NetworkPacket From(TransportSegment&& segment);
constexpr uint32_t length() const {
return GetHeaderField<uint32_t>(*mbuf_,
offsetof(DemoNetworkHeader, length));
}
private:
friend class LinkFrame;
friend class TransportSegment;
constexpr explicit NetworkPacket(pw::MultiBuf&& mbuf)
: mbuf_(std::move(mbuf)) {
PW_CHECK_UINT_EQ(mbuf_->NumLayers(), 3u);
size_t length = mbuf_->size();
PW_CHECK_UINT_LE(length, std::numeric_limits<uint32_t>::max());
SetHeaderField<uint32_t>(*mbuf_,
offsetof(DemoNetworkHeader, length),
static_cast<uint32_t>(length));
}
pw::MultiBuf::Instance mbuf_;
};
class TransportSegment {
public:
/// Convert a network packet to the transport segment it holds.
static TransportSegment From(NetworkPacket&& packet);
constexpr uint64_t id() const {
return GetHeaderField<uint64_t>(*mbuf_,
offsetof(DemoTransportHeader, segment_id));
}
constexpr uint32_t length() const {
return GetHeaderField<uint32_t>(*mbuf_,
offsetof(DemoTransportHeader, length));
}
constexpr pw::ByteSpan payload() {
return mbuf_->Chunks().begin()->subspan(DemoTransportHeader::kLength);
}
constexpr void set_id(uint64_t id) {
SetHeaderField<uint64_t>(
*mbuf_, offsetof(DemoTransportHeader, segment_id), id);
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-transport_segment-payload]
/// `TransportSegment::CopyFrom` writes a given message into the transport
/// segment's payload, and then updates the segment length to match.
void CopyFrom(const char* msg, size_t msg_len) {
uint32_t length = DemoTransportHeader::kLength;
size_t copied =
mbuf_->CopyFrom(as_bytes(pw::span<const char>(msg, msg_len)), length);
PW_CHECK_UINT_EQ(copied, msg_len);
length += static_cast<uint32_t>(msg_len);
mbuf_->TruncateTopLayer(length);
return SetHeaderField<uint32_t>(
*mbuf_, offsetof(DemoTransportHeader, length), length);
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-transport_segment-payload]
private:
friend class NetworkPacket;
constexpr explicit TransportSegment(pw::MultiBuf&& mbuf)
: mbuf_(std::move(mbuf)) {
PW_CHECK_UINT_EQ(mbuf_->NumLayers(), 4u);
size_t length = mbuf_->size();
PW_CHECK_UINT_LE(length, std::numeric_limits<uint32_t>::max());
SetHeaderField<uint32_t>(*mbuf_,
offsetof(DemoTransportHeader, length),
static_cast<uint32_t>(length));
}
pw::MultiBuf::Instance mbuf_;
};
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-from]
LinkFrame LinkFrame::From(NetworkPacket&& packet) {
size_t length =
packet.length() + DemoLinkHeader::kLength + DemoLinkFooter::kLength;
PW_CHECK_UINT_LE(length, std::numeric_limits<uint16_t>::max());
pw::MultiBuf::Instance mbuf = std::move(*packet.mbuf_);
mbuf->PopLayer();
mbuf->TruncateTopLayer(length);
return LinkFrame(std::move(*mbuf));
}
NetworkPacket NetworkPacket::From(LinkFrame&& frame) {
size_t length =
frame.length() - (DemoLinkHeader::kLength + DemoLinkFooter::kLength);
pw::MultiBuf::Instance mbuf = std::move(*frame.mbuf_);
PW_CHECK(mbuf->AddLayer(DemoLinkHeader::kLength, length));
return NetworkPacket(std::move(*mbuf));
}
NetworkPacket NetworkPacket::From(TransportSegment&& segment) {
size_t length = segment.length() + DemoNetworkHeader::kLength;
pw::MultiBuf::Instance mbuf = std::move(*segment.mbuf_);
mbuf->PopLayer();
mbuf->TruncateTopLayer(length);
return NetworkPacket(std::move(*mbuf));
}
TransportSegment TransportSegment::From(NetworkPacket&& packet) {
size_t length = packet.length() - DemoNetworkHeader::kLength;
pw::MultiBuf::Instance mbuf = std::move(*packet.mbuf_);
PW_CHECK(mbuf->AddLayer(DemoNetworkHeader::kLength, length));
return TransportSegment(std::move(*mbuf));
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-from]
template <typename FromProtocol, typename ToProtocol>
class Transformer : public pw::async2::Task {
public:
constexpr explicit Transformer(pw::log::Token name)
: pw::async2::Task(name) {}
template <typename U>
void ConnectTo(Transformer<ToProtocol, U>& next) {
std::tie(std::ignore, tx_, next.rx_) =
pw::async2::CreateSpscChannel<ToProtocol>(storage_);
}
protected:
// Exposes the send queue to derived classes. Can be used to inject messages.
pw::async2::Sender<ToProtocol>& tx() { return tx_; }
private:
template <typename, typename>
friend class Transformer;
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-transformer]
/// `Transformer::DoPend` reads from `rx_` and sends to `tx_`.
pw::async2::Poll<> DoPend(pw::async2::Context& context) override {
while (true) {
// First, send any previously transformed value.
if (tx_future_.is_pendable()) {
PW_TRY_READY_ASSIGN(bool sent, tx_future_.Pend(context));
if (!sent) {
break;
}
}
// Get a future read a message from the incoming channel.
if (!rx_future_.is_pendable()) {
rx_future_ = rx_.Receive();
}
// Try to resolve the future to a value.
PW_TRY_READY_ASSIGN(auto pending, rx_future_.Pend(context));
// Check if the channel has closed.
if (!pending.has_value()) {
break;
}
// Transform the message.
auto result = Transform(std::move(*pending));
if (!result.has_value()) {
break;
}
// Create a future to send it.
tx_future_ = tx_.Send(std::move(*result));
}
rx_.Disconnect();
tx_.Disconnect();
return pw::async2::Ready();
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-transformer]
virtual std::optional<ToProtocol> Transform(FromProtocol&& msg) = 0;
pw::async2::Receiver<FromProtocol> rx_;
pw::async2::ReceiveFuture<FromProtocol> rx_future_;
pw::async2::ChannelStorage<ToProtocol, kCapacity> storage_;
pw::async2::Sender<ToProtocol> tx_;
pw::async2::SendFuture<ToProtocol> tx_future_;
};
template <typename FromProtocol, typename ToProtocol>
class Relay : public Transformer<FromProtocol, ToProtocol> {
public:
constexpr explicit Relay(pw::log::Token name)
: Transformer<FromProtocol, ToProtocol>(name) {}
private:
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-relay]
/// `Relay::Transform` converts messages of one protocol into another.
std::optional<ToProtocol> Transform(FromProtocol&& msg) override {
return std::make_optional(ToProtocol::From(std::move(msg)));
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-relay]
};
class Encryptor : public Transformer<TransportSegment, TransportSegment> {
public:
constexpr Encryptor(pw::log::Token name, uint64_t key)
: Transformer<TransportSegment, TransportSegment>(name), key_(key) {}
private:
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-encryptor]
/// `Encryptor::Transform` "encrypts" the message. "Encrypting" again with the
/// same key is equivalent to decrypting.
std::optional<TransportSegment> Transform(
TransportSegment&& segment) override {
pw::random::XorShiftStarRng64 rng(key_ ^ segment.id());
std::array<std::byte, sizeof(uint64_t)> pad;
pw::ByteSpan payload = segment.payload();
for (size_t i = 0; i < payload.size(); ++i) {
if ((i % pad.size()) == 0) {
rng.Get(pad);
}
payload[i] ^= pad[i % pad.size()];
}
return std::make_optional(std::move(segment));
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-encryptor]
const uint64_t key_;
};
class LineTransformer : public Transformer<TransportSegment, TransportSegment> {
public:
constexpr LineTransformer(pw::log::Token name, pw::span<const char*>& lines)
: Transformer<TransportSegment, TransportSegment>(name),
lines_(lines),
iter_(lines.begin()) {}
[[nodiscard]] constexpr bool completed() const {
return iter_ == lines_.end();
}
private:
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-line_transformer]
/// `LineTransformer::Transform` acts sequentially of lines of a message.
std::optional<TransportSegment> Transform(
TransportSegment&& segment) override {
if (completed()) {
return std::nullopt;
}
TransformLine(*iter_++, segment);
return std::make_optional(std::move(segment));
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-line_transformer]
virtual void TransformLine(const char* line, TransportSegment& segment) = 0;
pw::span<const char*> lines_;
pw::span<const char*>::iterator iter_;
};
class Producer : public LineTransformer {
public:
constexpr Producer(pw::log::Token name, pw::span<const char*>& lines)
: LineTransformer(name, lines) {}
private:
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-producer]
/// `Producer::TransformLine` writes lines into payloads to be sent.
void TransformLine(const char* line, TransportSegment& segment) override {
segment.CopyFrom(line, strlen(line) + 1);
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-producer]
};
class Consumer : public LineTransformer {
public:
constexpr Consumer(pw::log::Token name, pw::span<const char*>& lines)
: LineTransformer(name, lines) {}
private:
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-consumer]
/// `Consumer::TransformLine` verifies received lines match expected ones.
void TransformLine(const char* line, TransportSegment& segment) override {
auto payload = pw::span_cast<char>(segment.payload());
EXPECT_STREQ(line, payload.data());
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-consumer]
};
class Recycler : public Transformer<TransportSegment, TransportSegment> {
public:
constexpr Recycler(pw::log::Token name)
: Transformer<TransportSegment, TransportSegment>(name) {}
/// Injects a new segment into the recycler.
pw::Status AddTransportSegment(TransportSegment&& segment) {
return tx().TrySend(Launder(std::move(segment)));
}
private:
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-recycler]
/// `Recycler::Transform` turns sent segments into freshly initialized ones.
std::optional<TransportSegment> Transform(
TransportSegment&& segment) override {
return std::make_optional(Launder(std::move(segment)));
}
TransportSegment Launder(TransportSegment&& segment) {
auto packet = NetworkPacket::From(std::move(segment));
auto frame = LinkFrame::From(std::move(packet));
frame.Reset();
packet = NetworkPacket::From(std::move(frame));
segment = TransportSegment::From(std::move(packet));
segment.set_id(next_segment_id_++);
return segment;
}
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-recycler]
uint64_t next_segment_id_ = 0x1000;
};
// Excerpt from the public domain poem by Vachel Lindsay.
const char* kTheAmaranth[] = {
"Ah, in the night, all music haunts me here....",
"Is it for naught high Heaven cracks and yawns",
"And the tremendous Amaranth descends",
"Sweet with the glory of ten thousand dawns?",
"Does it not mean my God would have me say: -",
"'Whether you will or no, O city young,",
"Heaven will bloom like one great flower for you,",
"Flash and loom greatly all your marts among?'",
"Friends, I will not cease hoping though you weep.",
"Such things I see, and some of them shall come",
"Though now our streets are harsh and ashen-gray,",
"Though our strong youths are strident now, or dumb.",
"Friends, that sweet torn, that wonder-town, shall rise.",
"Naught can delay it. Though it may not be",
"Just as I dream, it comes at last I know",
"With streets like channels of an incense-sea.",
};
constexpr size_t kNumLines = sizeof(kTheAmaranth) / sizeof(kTheAmaranth[0]);
TEST(PseudoEncrypt, RoundTrip) {
pw::allocator::test::AllocatorForTest<2048> allocator;
pw::async2::BasicDispatcher dispatcher;
pw::span<const char*> poem(kTheAmaranth, kNumLines);
constexpr uint64_t kKey = 0xDEADBEEFFEEDFACEull;
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-e2e]
// Instantiate the sending tasks.
Producer producer(PW_ASYNC_TASK_NAME("producer"), poem);
Encryptor encryptor(PW_ASYNC_TASK_NAME("encryptor"), kKey);
producer.ConnectTo(encryptor);
Relay<TransportSegment, NetworkPacket> net_sender(
PW_ASYNC_TASK_NAME("net_sender"));
encryptor.ConnectTo(net_sender);
Relay<NetworkPacket, LinkFrame> link_sender(
PW_ASYNC_TASK_NAME("link_sender"));
net_sender.ConnectTo(link_sender);
// Instantiate the receiving tasks.
Relay<LinkFrame, NetworkPacket> link_receiver(
PW_ASYNC_TASK_NAME("link_receiver"));
link_sender.ConnectTo(link_receiver);
Relay<NetworkPacket, TransportSegment> net_receiver(
PW_ASYNC_TASK_NAME("net_receiver"));
link_receiver.ConnectTo(net_receiver);
Encryptor decryptor(PW_ASYNC_TASK_NAME("decryptor"), kKey);
net_receiver.ConnectTo(decryptor);
Consumer consumer(PW_ASYNC_TASK_NAME("consumer"), poem);
decryptor.ConnectTo(consumer);
// And finally, instantiate a task to recycle messages back to the producer.
Recycler recycler(PW_ASYNC_TASK_NAME("recycler"));
consumer.ConnectTo(recycler);
recycler.ConnectTo(producer);
// Add some blank segments for the recycler to vend.
std::array<std::array<std::byte, kMaxDemoLinkFrameLength>, kCapacity> buffers;
for (auto& buffer : buffers) {
auto frame = LinkFrame::Create(allocator, buffer);
PW_CHECK_OK(frame.status());
auto packet = NetworkPacket::From(std::move(*frame));
auto segment = TransportSegment::From(std::move(packet));
PW_CHECK_OK(recycler.AddTransportSegment(std::move(segment)));
}
// Run all tasks on the dispatcher.
dispatcher.Post(producer);
dispatcher.Post(encryptor);
dispatcher.Post(net_sender);
dispatcher.Post(link_sender);
dispatcher.Post(link_receiver);
dispatcher.Post(net_receiver);
dispatcher.Post(decryptor);
dispatcher.Post(consumer);
dispatcher.Post(recycler);
// DOCSTAG: [pw_multibuf-examples-pseudo_encrypt-e2e]
dispatcher.RunToCompletion();
EXPECT_TRUE(producer.completed());
EXPECT_TRUE(consumer.completed());
}
} // namespace examples