Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore

**Default:** Varies by board

**Note:** Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte.
**Note:** Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary.

---

Expand Down
14 changes: 8 additions & 6 deletions src/helpers/AdvertDataHelpers.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/UTF8Helpers.h>

uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) {
app_data[0] = _type;
Expand All @@ -16,11 +17,12 @@
app_data[0] |= ADV_FEAT2_MASK;
memcpy(&app_data[i], &_extra2, 2); i += 2;
}
if (_name && *_name != 0) {
app_data[0] |= ADV_NAME_MASK;
const char* sp = _name;
while (*sp && i < MAX_ADVERT_DATA_SIZE) {
app_data[i++] = *sp++;
if (_name && *_name != 0) {
size_t name_len = mesh::validUtf8PrefixLength(_name, MAX_ADVERT_DATA_SIZE - i);
if (name_len > 0) {
app_data[0] |= ADV_NAME_MASK;
memcpy(&app_data[i], _name, name_len);
i += name_len;
}
}
return i;
Expand Down Expand Up @@ -84,4 +86,4 @@ void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_
}
}
}
}
}
56 changes: 56 additions & 0 deletions src/helpers/UTF8Helpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

#include <stddef.h>
#include <stdint.h>

namespace mesh {

inline bool isUtf8Continuation(uint8_t byte) {
return (byte & 0xC0) == 0x80;
}

inline size_t validUtf8PrefixLength(const char* text, size_t max_bytes) {
if (text == nullptr) return 0;

size_t offset = 0;
while (text[offset] != '\0') {
const uint8_t first = static_cast<uint8_t>(text[offset]);
size_t sequence_length = 0;

if (first <= 0x7F) {
sequence_length = 1;
} else if (first >= 0xC2 && first <= 0xDF) {
sequence_length = 2;
} else if (first >= 0xE0 && first <= 0xEF) {
sequence_length = 3;
} else if (first >= 0xF0 && first <= 0xF4) {
sequence_length = 4;
} else {
break;
}

if (offset + sequence_length > max_bytes) break;

bool complete = true;
for (size_t i = 1; i < sequence_length; i++) {
if (text[offset + i] == '\0' || !isUtf8Continuation(static_cast<uint8_t>(text[offset + i]))) {
complete = false;
break;
}
}
if (!complete) break;

if (sequence_length == 3) {
const uint8_t second = static_cast<uint8_t>(text[offset + 1]);
if ((first == 0xE0 && second < 0xA0) || (first == 0xED && second > 0x9F)) break;
} else if (sequence_length == 4) {
const uint8_t second = static_cast<uint8_t>(text[offset + 1]);
if ((first == 0xF0 && second < 0x90) || (first == 0xF4 && second > 0x8F)) break;
}

offset += sequence_length;
}
return offset;
}

} // namespace mesh
38 changes: 38 additions & 0 deletions test/test_utf8_helpers/test_utf8_helpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include <gtest/gtest.h>

#include <helpers/UTF8Helpers.h>

TEST(UTF8Helpers, KeepsCompleteNameWithinLimit) {
const char* name = "Example RPT 🔋🇵🇱";

EXPECT_EQ(24u, mesh::validUtf8PrefixLength(name, 24));
}

TEST(UTF8Helpers, StopsBeforeCodePointCrossingLimit) {
const char* name = "Example RPT 🔋🇵🇱";

EXPECT_EQ(20u, mesh::validUtf8PrefixLength(name, 23));
}

TEST(UTF8Helpers, RejectsMalformedAndTruncatedSequences) {
const char overlong[] = {'A', static_cast<char>(0xC0), static_cast<char>(0xAF), 0};
const char surrogate[] = {'A', static_cast<char>(0xED), static_cast<char>(0xA0), static_cast<char>(0x80), 0};
const char out_of_range[] = {'A', static_cast<char>(0xF4), static_cast<char>(0x90), static_cast<char>(0x80), static_cast<char>(0x80), 0};
const char truncated[] = {'A', static_cast<char>(0xF0), static_cast<char>(0x9F), 0};

EXPECT_EQ(1u, mesh::validUtf8PrefixLength(overlong, sizeof(overlong)));
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(surrogate, sizeof(surrogate)));
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(out_of_range, sizeof(out_of_range)));
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(truncated, sizeof(truncated)));
}

TEST(UTF8Helpers, RejectsUnexpectedContinuationByte) {
const char invalid[] = {'A', static_cast<char>(0x80), 'B', 0};

EXPECT_EQ(1u, mesh::validUtf8PrefixLength(invalid, sizeof(invalid)));
}

int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}