Files
dawn/tests/test-lxdr.cpp
2026-09-27 20:19:53 +02:00

506 lines
14 KiB
C++

//
// test-lxdr.cpp: LibertyXDR encode/decode and generated instance types
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "test.hpp"
#include <libdn/ipc.hpp>
#include "ipc/imaged.lxdr.hpp"
#include "ipc/instance.lxdr.hpp"
#include "ipc/thumbd.lxdr.hpp"
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <span>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
using namespace std;
namespace inst = dawn::ipc::instance;
namespace imaged = dawn::ipc::imaged;
namespace thumbd = dawn::ipc::thumbd;
template <typename T>
static vector<byte>
wire(const T &value)
{
vector<byte> buf;
dawn::ipc::Encoder enc(buf);
encode(value, enc);
CHECK(enc.ok());
return buf;
}
static bool
bytes_eq(span<const byte> got, span<const uint8_t> want)
{
if (got.size() != want.size())
return false;
for (size_t i = 0; i < got.size(); i++) {
if (uint8_t(got[i]) != want[i])
return false;
}
return true;
}
static void
check_golden(
const char *label, const vector<byte> &got, span<const uint8_t> want)
{
if (bytes_eq(got, want))
return;
test::fail("golden %s: got %llu bytes, want %llu", label,
(unsigned long long) got.size(), (unsigned long long) want.size());
for (size_t i = 0; i < got.size(); i++)
fprintf(stderr, " %02x", uint8_t(got[i]));
fprintf(stderr, "\n");
}
static void
test_round_trip_hello_reply()
{
{
dawn::ipc::HelloReply own;
own.value =
dawn::ipc::HelloReplyAccepted{dawn::ipc::Limits{0x00100000u}};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
dawn::ipc::HelloReplyView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<dawn::ipc::HelloReplyAcceptedView>(view.value));
CHECK(get<dawn::ipc::HelloReplyAcceptedView>(view.value)
.limits.max_payload_size == 0x00100000u);
}
{
dawn::ipc::HelloReply own;
own.value = dawn::ipc::HelloReplyVersionMismatch{2};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
dawn::ipc::HelloReplyView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<dawn::ipc::HelloReplyVersionMismatchView>(
view.value));
CHECK(get<dawn::ipc::HelloReplyVersionMismatchView>(view.value)
.server_protocol_version == 2);
}
{
dawn::ipc::HelloReply own;
own.value = dawn::ipc::HelloReplySessionMismatch{};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
dawn::ipc::HelloReplyView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<dawn::ipc::HelloReplySessionMismatchView>(
view.value));
}
}
static void
test_round_trip_result()
{
{
inst::Result own;
own.value = inst::ResultDone{};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
inst::ResultView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<inst::ResultDoneView>(view.value));
}
{
inst::Result own;
own.value = inst::ResultError{
dawn::ipc::Error{dawn::ipc::ErrorCode::Busy, "nope"}};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
inst::ResultView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<inst::ResultErrorView>(view.value));
const auto &err = get<inst::ResultErrorView>(view.value).error;
CHECK(err.code == dawn::ipc::ErrorCode::Busy);
CHECK(err.message == "nope");
}
}
static void
test_round_trip_frames()
{
{
dawn::ipc::Hello hello{1, "sess"};
const vector<byte> buf = wire(hello);
dawn::ipc::Decoder dec(buf);
dawn::ipc::HelloView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(view.protocol_version == 1);
CHECK(view.session == "sess");
}
{
inst::OpenRequest open;
open.urls = {"/one", "/two"};
open.activation_token = "tok";
open.mode = "browse";
const vector<byte> buf = wire(open);
dawn::ipc::Decoder dec(buf);
inst::OpenRequestView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(view.urls.size() == 2);
CHECK(view.urls[0] == "/one");
CHECK(view.urls[1] == "/two");
CHECK(view.activation_token == "tok");
CHECK(view.mode == "browse");
}
{
inst::Frame own;
own.payload.value = inst::PayloadHello{dawn::ipc::Hello{1, "b"}};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
inst::FrameView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<inst::PayloadHelloView>(view.payload.value));
const auto &h = get<inst::PayloadHelloView>(view.payload.value).hello;
CHECK(h.protocol_version == 1);
CHECK(h.session == "b");
}
{
dawn::ipc::HelloReply reply;
reply.value = dawn::ipc::HelloReplyAccepted{dawn::ipc::Limits{4096}};
inst::Frame own;
own.payload.value = inst::PayloadHelloReply{reply};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
inst::FrameView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(
holds_alternative<inst::PayloadHelloReplyView>(view.payload.value));
const auto &hr =
get<inst::PayloadHelloReplyView>(view.payload.value).hello_reply;
CHECK(holds_alternative<dawn::ipc::HelloReplyAcceptedView>(hr.value));
CHECK(get<dawn::ipc::HelloReplyAcceptedView>(hr.value)
.limits.max_payload_size == 4096);
}
{
inst::OpenRequest open;
open.urls = {"/p", "/q"};
open.activation_token = "";
inst::Request req;
req.id = 7;
req.body.value = inst::RequestBodyOpen{open};
inst::Frame own;
own.payload.value = inst::PayloadRequest{req};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
inst::FrameView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<inst::PayloadRequestView>(view.payload.value));
const auto &r =
get<inst::PayloadRequestView>(view.payload.value).request;
CHECK(r.id == 7);
CHECK(holds_alternative<inst::RequestBodyOpenView>(r.body.value));
const auto &o = get<inst::RequestBodyOpenView>(r.body.value).open;
CHECK(o.urls.size() == 2);
CHECK(o.urls[0] == "/p");
CHECK(o.urls[1] == "/q");
CHECK(o.mode.empty());
}
{
inst::Response resp;
resp.id = 9;
resp.result.value = inst::ResultDone{};
inst::Frame own;
own.payload.value = inst::PayloadResponse{resp};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
inst::FrameView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<inst::PayloadResponseView>(view.payload.value));
const auto &r =
get<inst::PayloadResponseView>(view.payload.value).response;
CHECK(r.id == 9);
CHECK(holds_alternative<inst::ResultDoneView>(r.result.value));
}
{
inst::Frame own;
own.payload.value =
inst::PayloadCancel{dawn::ipc::Cancel{0x100000002ull}};
const vector<byte> buf = wire(own);
dawn::ipc::Decoder dec(buf);
inst::FrameView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(holds_alternative<inst::PayloadCancelView>(view.payload.value));
CHECK(get<inst::PayloadCancelView>(view.payload.value).cancel.id ==
0x100000002ull);
}
}
static void
test_goldens()
{
// Hello{1, "b"}: u32be version, u32be len+"b"
static constexpr uint8_t kHello[] = {
0x00,
0x00,
0x00,
0x01, // protocol_version = 1
0x00,
0x00,
0x00,
0x01, // session length
0x62, // 'b'
};
check_golden("Hello{1,\"b\"}", wire(dawn::ipc::Hello{1, "b"}), kHello);
// HelloReply SessionMismatch: tag i8 = 3, no payload
static constexpr uint8_t kSessionMismatch[] = {0x03};
dawn::ipc::HelloReply mismatch;
mismatch.value = dawn::ipc::HelloReplySessionMismatch{};
check_golden(
"HelloReply SessionMismatch", wire(mismatch), kSessionMismatch);
// Error{NotFound, "x"}: i8 code = 3, u32be len+"x"
static constexpr uint8_t kError[] = {
0x03, // ErrorCode::NotFound
0x00,
0x00,
0x00,
0x01, // message length
0x78, // 'x'
};
check_golden("Error{NotFound,\"x\"}",
wire(dawn::ipc::Error{dawn::ipc::ErrorCode::NotFound, "x"}), kError);
}
static void
test_truncation()
{
const vector<byte> full = wire(dawn::ipc::Hello{1, "b"});
CHECK(!full.empty());
for (size_t n = 0; n < full.size(); n++) {
dawn::ipc::Decoder dec(span<const byte>(full.data(), n));
dawn::ipc::HelloView view{};
const bool ok = decode(dec, view);
CHECK(!ok);
CHECK(dec.error() == dawn::ipc::DecodeError::Truncated);
}
dawn::ipc::Decoder dec(full);
dawn::ipc::HelloView view{};
CHECK(decode(dec, view));
CHECK(dec.remaining() == 0);
CHECK(dec.error() == dawn::ipc::DecodeError::Ok);
CHECK(view.protocol_version == 1);
CHECK(view.session == "b");
}
static void
test_trailing_bytes()
{
vector<byte> buf = wire(dawn::ipc::Hello{1, "b"});
buf.push_back(byte{0x00});
dawn::ipc::Decoder dec(buf);
dawn::ipc::HelloView view{};
const bool ok = decode(dec, view);
// Generated Hello decode succeeds and leaves the extra byte.
// Connection/Server require exact consumption.
CHECK(!ok || dec.remaining() != 0);
}
static void
test_invalid_utf8()
{
{
// u32be length 2, overlong NUL (C0 80)
const uint8_t raw[] = {0x00, 0x00, 0x00, 0x02, 0xC0, 0x80};
dawn::ipc::Decoder dec(as_bytes(span(raw)));
string_view s;
CHECK(!dec.string(s));
CHECK(dec.error() == dawn::ipc::DecodeError::InvalidUtf8);
}
{
// u32be length 1, truncated 2-byte sequence
const uint8_t raw[] = {0x00, 0x00, 0x00, 0x01, 0xC2};
dawn::ipc::Decoder dec(as_bytes(span(raw)));
string_view s;
CHECK(!dec.string(s));
CHECK(dec.error() == dawn::ipc::DecodeError::InvalidUtf8);
}
}
static void
test_zero_and_unknown_enum()
{
for (byte tag : {byte{0}, byte{99}}) {
const byte raw[] = {tag};
dawn::ipc::Decoder dec(raw);
dawn::ipc::HelloResult value{};
CHECK(!decode(dec, value));
}
}
static void
test_unknown_union_tag()
{
const byte raw[] = {byte{99}};
dawn::ipc::Decoder dec(raw);
dawn::ipc::HelloReplyView view{};
CHECK(!decode(dec, view));
}
static void
test_huge_array_count()
{
const size_t n = dawn::ipc::Decoder::kMaxElements + 1;
{
const uint8_t raw[] = {0xFF, 0xFF, 0xFF, 0xFF};
dawn::ipc::Decoder dec(as_bytes(span(raw)));
inst::OpenRequestView view{};
CHECK(!decode(dec, view));
}
{
vector<byte> raw(n);
dawn::ipc::Decoder dec(raw);
span<const byte> out;
CHECK(!dec.bytes(out, n));
CHECK(dec.error() == dawn::ipc::DecodeError::Limit);
}
{
vector<byte> raw(4 + n, byte{0});
raw[0] = byte((n >> 24) & 0xff);
raw[1] = byte((n >> 16) & 0xff);
raw[2] = byte((n >> 8) & 0xff);
raw[3] = byte(n & 0xff);
dawn::ipc::Decoder dec(raw);
string_view sv;
CHECK(!dec.string(sv));
CHECK(dec.error() == dawn::ipc::DecodeError::Limit);
}
}
static void
test_daemon_schemas()
{
dawn::ipc::DaemonHelloReply hello;
hello.value = dawn::ipc::DaemonHelloReplyAccepted{
dawn::ipc::DaemonLimits{4096, imaged::kImagedMaxBlobSize}};
auto bytes = wire(hello);
dawn::ipc::Decoder hello_decoder(bytes);
dawn::ipc::DaemonHelloReplyView hello_view;
CHECK(decode(hello_decoder, hello_view));
const auto *accepted =
get_if<dawn::ipc::DaemonHelloReplyAcceptedView>(&hello_view.value);
CHECK(accepted != nullptr);
if (accepted)
CHECK(accepted->limits.max_blob_size == imaged::kImagedMaxBlobSize);
dawn::ipc::Pixmap pixmap;
pixmap.width = 2;
pixmap.height = 3;
pixmap.stride = 16;
pixmap.orientation = 6;
pixmap.pixels.value = dawn::ipc::BlobShared{48};
imaged::DecodeResponse response{pixmap, "wuffs", {}, true};
bytes = wire(response);
dawn::ipc::Decoder decoder(bytes);
imaged::DecodeResponseView view;
CHECK(decode(decoder, view));
CHECK(
holds_alternative<dawn::ipc::BlobSharedView>(view.pixmap.pixels.value));
CHECK(get<dawn::ipc::BlobSharedView>(view.pixmap.pixels.value).size == 48);
thumbd::ScaleResponse scaled;
scaled.width = 1;
scaled.height = 1;
scaled.rgba8.value =
dawn::ipc::BlobInline{{byte{1}, byte{2}, byte{3}, byte{4}}};
bytes = wire(scaled);
dawn::ipc::Decoder decoder2(bytes);
thumbd::ScaleResponseView scaled_view;
CHECK(decode(decoder2, scaled_view));
CHECK(
holds_alternative<dawn::ipc::BlobInlineView>(scaled_view.rgba8.value));
CHECK(
get<dawn::ipc::BlobInlineView>(scaled_view.rgba8.value).bytes.size() ==
4);
}
static void
test_blob_envelope_sizes()
{
imaged::DecodeRequest decode;
decode.data.value = dawn::ipc::BlobInline{};
imaged::Frame request;
request.payload.value = imaged::PayloadRequest{imaged::Request{1, decode}};
const size_t imaged_overhead = wire(request).size();
get<imaged::PayloadRequest>(request.payload.value)
.request.decode.data.value =
dawn::ipc::BlobInline{vector<byte>(123, byte{1})};
CHECK(wire(request).size() == imaged_overhead + 123);
imaged::DecodeResponse decoded;
decoded.pixmap.width = 2;
decoded.pixmap.height = 2;
decoded.pixmap.stride = 16;
decoded.pixmap.pixels.value = dawn::ipc::BlobInline{};
decoded.loader = "variable-loader";
decoded.icc = vector<byte>(31, byte{2});
imaged::Frame decoded_frame;
decoded_frame.payload.value = imaged::PayloadResponse{
imaged::Response{2, imaged::Result{imaged::ResultDecoded{decoded}}}};
const size_t decoded_overhead = wire(decoded_frame).size();
get<imaged::ResultDecoded>(
get<imaged::PayloadResponse>(decoded_frame.payload.value)
.response.result.value)
.decoded.pixmap.pixels.value =
dawn::ipc::BlobInline{vector<byte>(89, byte{3})};
CHECK(wire(decoded_frame).size() == decoded_overhead + 89);
thumbd::ScaleResponse scale;
scale.width = 1;
scale.height = 1;
scale.rgba8.value = dawn::ipc::BlobInline{};
thumbd::Frame response;
response.payload.value = thumbd::PayloadResponse{
thumbd::Response{1, thumbd::Result{thumbd::ResultScaled{scale}}}};
const size_t thumbd_overhead = wire(response).size();
get<thumbd::PayloadResponse>(response.payload.value).response.result.value =
thumbd::ResultScaled{thumbd::ScaleResponse{
1, 1, dawn::ipc::Blob{dawn::ipc::BlobInline{vector<byte>(77)}}}};
CHECK(wire(response).size() == thumbd_overhead + 77);
}
int
main()
{
return test::run({
{"Hello reply round trips", test_round_trip_hello_reply},
{"result round trips", test_round_trip_result},
{"frame round trips", test_round_trip_frames},
{"golden encodings", test_goldens},
{"truncated input", test_truncation},
{"trailing input", test_trailing_bytes},
{"invalid UTF-8", test_invalid_utf8},
{"invalid enum", test_zero_and_unknown_enum},
{"unknown union tag", test_unknown_union_tag},
{"size limits", test_huge_array_count},
{"daemon schemas", test_daemon_schemas},
{"blob envelope sizes", test_blob_envelope_sizes},
});
}