blob: 03ddbe8e50f5632f690eabdff71c0bb6c20868c8 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "discovery/mdns/mdns_writer.h"
#include <memory>
#include <vector>
#include "discovery/mdns/testing/mdns_test_util.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace openscreen {
namespace discovery {
using testing::ElementsAreArray;
namespace {
constexpr std::chrono::seconds kTtl{120};
template <class T>
void TestWriteEntrySucceeds(const T& entry,
const uint8_t* expected_data,
size_t expected_size) {
std::vector<uint8_t> buffer(expected_size);
MdnsWriter writer(buffer.data(), buffer.size());
EXPECT_TRUE(writer.Write(entry));
EXPECT_EQ(writer.remaining(), UINT64_C(0));
EXPECT_THAT(buffer, ElementsAreArray(expected_data, expected_size));
}
template <class T>
void TestWriteEntryInsufficientBuffer(const T& entry) {
std::vector<uint8_t> buffer(entry.MaxWireSize() - 1);
MdnsWriter writer(buffer.data(), buffer.size());
EXPECT_FALSE(writer.Write(entry));
// There should be no side effects for failing to write an entry. The
// underlying pointer should not have changed.
EXPECT_EQ(writer.offset(), UINT64_C(0));
}
} // namespace
TEST(MdnsWriterTest, WriteDomainName) {
// clang-format off
constexpr uint8_t kExpectedResult[] = {
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00
};
// clang-format on
uint8_t result[sizeof(kExpectedResult)];
MdnsWriter writer(result, sizeof(kExpectedResult));
ASSERT_TRUE(writer.Write(DomainName{"testing", "local"}));
EXPECT_EQ(0UL, writer.remaining());
EXPECT_EQ(0, memcmp(kExpectedResult, result, sizeof(result)));
}
TEST(MdnsWriterTest, WriteDomainName_CompressedMessage) {
// clang-format off
constexpr uint8_t kExpectedResultCompressed[] = {
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
0x06, 'p', 'r', 'e', 'f', 'i', 'x',
0xC0, 0x08, // byte 8
0x03, 'n', 'e', 'w',
0xC0, 0x0F, // byte 15
0xC0, 0x0F, // byte 15
};
// clang-format on
uint8_t result[sizeof(kExpectedResultCompressed)];
MdnsWriter writer(result, sizeof(kExpectedResultCompressed));
ASSERT_TRUE(writer.Write(DomainName{"testing", "local"}));
ASSERT_TRUE(writer.Write(DomainName{"prefix", "local"}));
ASSERT_TRUE(writer.Write(DomainName{"new", "prefix", "local"}));
ASSERT_TRUE(writer.Write(DomainName{"prefix", "local"}));
EXPECT_EQ(0UL, writer.remaining());
EXPECT_THAT(std::vector<uint8_t>(result, result + sizeof(result)),
ElementsAreArray(kExpectedResultCompressed));
}
TEST(MdnsWriterTest, WriteDomainName_NotEnoughSpace) {
// clang-format off
constexpr uint8_t kExpectedResultCompressed[] = {
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
0x09, 'd', 'i', 'f', 'f', 'e', 'r', 'e', 'n', 't',
0x06, 'd', 'o', 'm', 'a', 'i', 'n',
0x00
};
// clang-format on
uint8_t result[sizeof(kExpectedResultCompressed)];
MdnsWriter writer(result, sizeof(kExpectedResultCompressed));
ASSERT_TRUE(writer.Write(DomainName{"testing", "local"}));
// Not enough space to write this domain name. Failure to write it must not
// affect correct successful write of the next domain name.
ASSERT_FALSE(writer.Write(DomainName{"a", "different", "domain"}));
ASSERT_TRUE(writer.Write(DomainName{"different", "domain"}));
EXPECT_EQ(0UL, writer.remaining());
EXPECT_THAT(std::vector<uint8_t>(result, result + sizeof(result)),
ElementsAreArray(kExpectedResultCompressed));
}
TEST(MdnsWriterTest, WriteDomainName_Long) {
constexpr char kLongLabel[] =
"12345678901234567890123456789012345678901234567890";
// clang-format off
constexpr uint8_t kExpectedResult[] = {
0x32, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
0x32, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
0x32, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
0x32, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
0x00,
};
// clang-format on
DomainName name{kLongLabel, kLongLabel, kLongLabel, kLongLabel};
uint8_t result[sizeof(kExpectedResult)];
MdnsWriter writer(result, sizeof(kExpectedResult));
ASSERT_TRUE(writer.Write(name));
EXPECT_EQ(0UL, writer.remaining());
EXPECT_EQ(0, memcmp(kExpectedResult, result, sizeof(result)));
}
TEST(MdnsWriterTest, WriteDomainName_Empty) {
DomainName name;
uint8_t result[256];
MdnsWriter writer(result, sizeof(result));
EXPECT_FALSE(writer.Write(name));
// The writer should not have moved its internal pointer when it fails to
// write. It should fail without any side effects.
EXPECT_EQ(0u, writer.offset());
}
TEST(MdnsWriterTest, WriteDomainName_NoCompressionForBigOffsets) {
// clang-format off
constexpr uint8_t kExpectedResultCompressed[] = {
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
};
// clang-format on
DomainName name{"testing", "local"};
// Maximum supported value for label pointer offset is 0x3FFF.
// Labels written into a buffer at greater offsets must not
// produce compression label pointers.
std::vector<uint8_t> buffer(0x4000 + sizeof(kExpectedResultCompressed));
{
MdnsWriter writer(buffer.data(), buffer.size());
writer.Skip(0x4000);
ASSERT_TRUE(writer.Write(name));
ASSERT_TRUE(writer.Write(name));
EXPECT_EQ(0UL, writer.remaining());
}
buffer.erase(buffer.begin(), buffer.begin() + 0x4000);
EXPECT_THAT(buffer, ElementsAreArray(kExpectedResultCompressed));
}
TEST(MdnsWriterTest, WriteRawRecordRdata) {
// clang-format off
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x08, // RDLENGTH = 8 bytes
0x05, 'c', 'n', 'a', 'm', 'e', 0xc0, 0x00,
};
// clang-format on
TestWriteEntrySucceeds(
RawRecordRdata(kExpectedRdata + 2, sizeof(kExpectedRdata) - 2),
kExpectedRdata, sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WriteRawRecordRdata_InsufficientBuffer) {
// clang-format off
constexpr uint8_t kRawRdata[] = {
0x05, 'c', 'n', 'a', 'm', 'e', 0xc0, 0x00,
};
// clang-format on
TestWriteEntryInsufficientBuffer(
RawRecordRdata(kRawRdata, sizeof(kRawRdata)));
}
TEST(MdnsWriterTest, WriteSrvRecordRdata) {
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x15, // RDLENGTH = 21
0x00, 0x05, // PRIORITY = 5
0x00, 0x06, // WEIGHT = 6
0x1f, 0x49, // PORT = 8009
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l', 0x00,
};
TestWriteEntrySucceeds(
SrvRecordRdata(5, 6, 8009, DomainName{"testing", "local"}),
kExpectedRdata, sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WriteSrvRecordRdata_InsufficientBuffer) {
TestWriteEntryInsufficientBuffer(
SrvRecordRdata(5, 6, 8009, DomainName{"testing", "local"}));
}
TEST(MdnsWriterTest, WriteARecordRdata) {
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x4, // RDLENGTH = 4
0x08, 0x08, 0x08, 0x08, // ADDRESS = 8.8.8.8
};
TestWriteEntrySucceeds(ARecordRdata(IPAddress{8, 8, 8, 8}), kExpectedRdata,
sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WriteARecordRdata_InsufficientBuffer) {
TestWriteEntryInsufficientBuffer(ARecordRdata(IPAddress{8, 8, 8, 8}));
}
TEST(MdnsWriterTest, WriteAAAARecordRdata) {
// clang-format off
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x10, // RDLENGTH = 16
// ADDRESS = FE80:0000:0000:0000:0202:B3FF:FE1E:8329
0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x02, 0xb3, 0xff, 0xfe, 0x1e, 0x83, 0x29,
};
// clang-format on
TestWriteEntrySucceeds(
AAAARecordRdata(IPAddress(IPAddress::Version::kV6, kExpectedRdata + 2)),
kExpectedRdata, sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WriteAAAARecordRdata_InsufficientBuffer) {
// clang-format off
constexpr uint16_t kAAAARdata[] = {
// ADDRESS = FE80:0000:0000:0000:0202:B3FF:FE1E:8329
0xfe80, 0x0000, 0x0000, 0x0000,
0x0202, 0xb3ff, 0xfe1e, 0x8329,
};
// clang-format on
TestWriteEntryInsufficientBuffer(AAAARecordRdata(IPAddress(kAAAARdata)));
}
TEST(MdnsWriterTest, WriteNSECRecordRdata) {
const DomainName domain{"testing", "local"};
NsecRecordRdata(DomainName{"mydevice", "testing", "local"}, DnsType::kA,
DnsType::kTXT, DnsType::kSRV, DnsType::kNSEC);
// clang-format off
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x20, // RDLENGTH = 32
0x08, 'm', 'y', 'd', 'e', 'v', 'i', 'c', 'e',
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
// It takes 8 bytes to encode the kA and kSRV records because:
// - Both record types have value less than 256, so they are both in window
// block 1.
// - The bitmap length for this block is always a single byte
// - DnsTypes have the following values:
// - kA = 1 (encoded in byte 1)
// kTXT = 16 (encoded in byte 3)
// - kSRV = 33 (encoded in byte 5)
// - kNSEC = 47 (encoded in 6 bytes)
// - The largest of these is 47, so 6 bytes are needed to encode this data.
// So the full encoded version is:
// 00000000 00000110 01000000 00000000 10000000 00000000 0100000 00000001
// |window| | size | | 0-7 | | 8-15 | |16-23 | |24-31 | |32-39 | |40-47 |
0x00, 0x06, 0x40, 0x00, 0x80, 0x00, 0x40, 0x01
};
// clang-format on
TestWriteEntrySucceeds(
NsecRecordRdata(DomainName{"mydevice", "testing", "local"}, DnsType::kA,
DnsType::kTXT, DnsType::kSRV, DnsType::kNSEC),
kExpectedRdata, sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WriteNSECRecordRdata_InsufficientBuffer) {
TestWriteEntryInsufficientBuffer(
NsecRecordRdata(DomainName{"mydevice", "testing", "local"}, DnsType::kA,
DnsType::kTXT, DnsType::kSRV, DnsType::kNSEC));
}
TEST(MdnsWriterTest, WritePtrRecordRdata) {
// clang-format off
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x18, // RDLENGTH = 24
0x08, 'm', 'y', 'd', 'e', 'v', 'i', 'c', 'e',
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
};
// clang-format on
TestWriteEntrySucceeds(
PtrRecordRdata(DomainName{"mydevice", "testing", "local"}),
kExpectedRdata, sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WritePtrRecordRdata_InsufficientBuffer) {
TestWriteEntryInsufficientBuffer(
PtrRecordRdata(DomainName{"mydevice", "testing", "local"}));
}
TEST(MdnsWriterTest, WriteTxtRecordRdata) {
// clang-format off
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x0C, // RDLENGTH = 12
0x05, 'f', 'o', 'o', '=', '1',
0x05, 'b', 'a', 'r', '=', '2',
};
// clang-format on
TestWriteEntrySucceeds(MakeTxtRecord({"foo=1", "bar=2"}), kExpectedRdata,
sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WriteTxtRecordRdata_Empty) {
constexpr uint8_t kExpectedRdata[] = {
0x00, 0x01, // RDLENGTH = 1
0x00, // empty string
};
TestWriteEntrySucceeds(TxtRecordRdata(), kExpectedRdata,
sizeof(kExpectedRdata));
}
TEST(MdnsWriterTest, WriteTxtRecordRdata_InsufficientBuffer) {
TestWriteEntryInsufficientBuffer(MakeTxtRecord({"foo=1", "bar=2"}));
}
TEST(MdnsWriterTest, WriteMdnsRecord_ARecordRdata) {
// clang-format off
constexpr uint8_t kExpectedResult[] = {
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
0x00, 0x01, // TYPE = A (1)
0x80, 0x01, // CLASS = IN (1) | CACHE_FLUSH_BIT
0x00, 0x00, 0x00, 0x78, // TTL = 120 seconds
0x00, 0x04, // RDLENGTH = 4 bytes
0xac, 0x00, 0x00, 0x01, // 172.0.0.1
};
// clang-format on
TestWriteEntrySucceeds(MdnsRecord(DomainName{"testing", "local"}, DnsType::kA,
DnsClass::kIN, RecordType::kUnique, kTtl,
ARecordRdata(IPAddress{172, 0, 0, 1})),
kExpectedResult, sizeof(kExpectedResult));
}
TEST(MdnsWriterTest, WriteMdnsRecord_PtrRecordRdata) {
// clang-format off
constexpr uint8_t kExpectedResult[] = {
0x08, '_', 's', 'e', 'r', 'v', 'i', 'c', 'e',
0x07, 't', 'e', 's', 't', 'i', 'n', 'g',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
0x00, 0x0c, // TYPE = PTR (12)
0x00, 0x01, // CLASS = IN (1)
0x00, 0x00, 0x00, 0x78, // TTL = 120 seconds
0x00, 0x02, // RDLENGTH = 2 bytes
0xc0, 0x09, // Domain name label pointer to byte
};
// clang-format on
TestWriteEntrySucceeds(
MdnsRecord(DomainName{"_service", "testing", "local"}, DnsType::kPTR,
DnsClass::kIN, RecordType::kShared, kTtl,
PtrRecordRdata(DomainName{"testing", "local"})),
kExpectedResult, sizeof(kExpectedResult));
}
TEST(MdnsWriterTest, WriteMdnsRecord_InsufficientBuffer) {
TestWriteEntryInsufficientBuffer(MdnsRecord(
DomainName{"testing", "local"}, DnsType::kA, DnsClass::kIN,
RecordType::kUnique, kTtl, ARecordRdata(IPAddress{172, 0, 0, 1})));
}
TEST(MdnsWriterTest, WriteMdnsQuestion) {
// clang-format off
constexpr uint8_t kExpectedResult[] = {
0x04, 'w', 'i', 'r', 'e',
0x06, 'f', 'o', 'r', 'm', 'a', 't',
0x05, 'l', 'o', 'c', 'a', 'l',
0x00,
0x00, 0x0c, // TYPE = PTR (12)
0x80, 0x01, // CLASS = IN (1) | UNICAST_BIT
};
// clang-format on
TestWriteEntrySucceeds(
MdnsQuestion(DomainName{"wire", "format", "local"}, DnsType::kPTR,
DnsClass::kIN, ResponseType::kUnicast),
kExpectedResult, sizeof(kExpectedResult));
}
TEST(MdnsWriterTest, WriteMdnsQuestion_InsufficientBuffer) {
TestWriteEntryInsufficientBuffer(
MdnsQuestion(DomainName{"wire", "format", "local"}, DnsType::kPTR,
DnsClass::kIN, ResponseType::kUnicast));
}
TEST(MdnsWriterTest, WriteMdnsMessage) {
// clang-format off
constexpr uint8_t kExpectedMessage[] = {
0x00, 0x01, // ID = 1
0x00, 0x00, // FLAGS = None
0x00, 0x01, // Question count
0x00, 0x00, // Answer count
0x00, 0x01, // Authority count
0x00, 0x00, // Additional count
// Question
0x08, 'q', 'u', 'e', 's', 't', 'i', 'o', 'n',
0x00,
0x00, 0x0c, // TYPE = PTR (12)
0x00, 0x01, // CLASS = IN (1)
// Authority Record
0x04, 'a', 'u', 't', 'h',
0x00,
0x00, 0x10, // TYPE = TXT (16)
0x00, 0x01, // CLASS = IN (1)
0x00, 0x00, 0x00, 0x78, // TTL = 120 seconds
0x00, 0x0c, // RDLENGTH = 12 bytes
0x05, 'f', 'o', 'o', '=', '1',
0x05, 'b', 'a', 'r', '=', '2',
};
// clang-format on
MdnsQuestion question(DomainName{"question"}, DnsType::kPTR, DnsClass::kIN,
ResponseType::kMulticast);
MdnsRecord auth_record(DomainName{"auth"}, DnsType::kTXT, DnsClass::kIN,
RecordType::kShared, kTtl,
MakeTxtRecord({"foo=1", "bar=2"}));
MdnsMessage message(1, MessageType::Query);
message.AddQuestion(question);
message.AddAuthorityRecord(auth_record);
std::vector<uint8_t> buffer(sizeof(kExpectedMessage));
MdnsWriter writer(buffer.data(), buffer.size());
EXPECT_TRUE(writer.Write(message));
EXPECT_EQ(writer.remaining(), UINT64_C(0));
EXPECT_THAT(buffer, ElementsAreArray(kExpectedMessage));
}
TEST(MdnsWriterTest, WriteMdnsMessage_InsufficientBuffer) {
MdnsQuestion question(DomainName{"question"}, DnsType::kPTR, DnsClass::kIN,
ResponseType::kMulticast);
MdnsRecord auth_record(DomainName{"auth"}, DnsType::kTXT, DnsClass::kIN,
RecordType::kShared, kTtl,
MakeTxtRecord({"foo=1", "bar=2"}));
MdnsMessage message(1, MessageType::Query);
message.AddQuestion(question);
message.AddAuthorityRecord(auth_record);
TestWriteEntryInsufficientBuffer(message);
}
} // namespace discovery
} // namespace openscreen