Expand full Companion terminal tooling

This commit is contained in:
mikecarper committed 2026-08-10 14:53:39 -07:00
1 parent 88ba5b26cb
commit 4e91cc9487
10 files changed
+1235 -41

No files matched your search

+111
View File
@@ -29,17 +29,128 @@ enum class RecentRepeaterGetMatch : uint8_t {
Invalid,
};
enum class TerminalChannelCommandMatch : uint8_t {
NoMatch = 0,
Valid,
MissingSelector,
MissingMessage,
};
enum class TerminalArgumentCommandMatch : uint8_t {
NoMatch = 0,
Valid,
MissingArgument,
};
struct RecentRepeaterGetQuery {
int page;
uint8_t search_prefix[3];
uint8_t search_prefix_len;
};
struct TerminalChannelMessage {
const char* selector;
size_t selector_len;
const char* text;
};
inline const char* skipRecentRepeaterSpaces(const char* text) {
while (text != nullptr && (*text == ' ' || *text == '\t')) text++;
return text;
}
inline TerminalArgumentCommandMatch parseTerminalArgumentCommand(
const char* command, const char* verb, const char*& argument) {
argument = nullptr;
if (command == nullptr || verb == nullptr || *verb == 0) {
return TerminalArgumentCommandMatch::NoMatch;
}
const size_t verb_len = strlen(verb);
for (size_t i = 0; i < verb_len; i++) {
char actual = command[i];
char expected = verb[i];
if (actual >= 'A' && actual <= 'Z') actual += 'a' - 'A';
if (expected >= 'A' && expected <= 'Z') expected += 'a' - 'A';
if (actual != expected) return TerminalArgumentCommandMatch::NoMatch;
}
if (command[verb_len] != 0 && command[verb_len] != ' '
&& command[verb_len] != '\t') {
return TerminalArgumentCommandMatch::NoMatch;
}
const char* cursor = skipRecentRepeaterSpaces(command + verb_len);
if (*cursor == 0) return TerminalArgumentCommandMatch::MissingArgument;
argument = cursor;
return TerminalArgumentCommandMatch::Valid;
}
// Return true once terminal input has reached a login password. The caller
// can still retain the real bytes for command handling while echoing '*'.
inline bool shouldMaskTerminalInput(const char* line) {
line = skipRecentRepeaterSpaces(line);
const char* password = nullptr;
return parseTerminalArgumentCommand(line, "login", password)
== TerminalArgumentCommandMatch::Valid;
}
inline TerminalChannelCommandMatch parseTerminalChannelMessage(
const char* command, TerminalChannelMessage& message) {
message.selector = nullptr;
message.selector_len = 0;
message.text = nullptr;
if (command == nullptr || strncmp(command, "channel", 7) != 0) {
return TerminalChannelCommandMatch::NoMatch;
}
const char* cursor = command + 7;
if (*cursor != 0 && *cursor != ' ' && *cursor != '\t') {
return TerminalChannelCommandMatch::NoMatch;
}
cursor = skipRecentRepeaterSpaces(cursor);
if (*cursor == 0) return TerminalChannelCommandMatch::MissingSelector;
message.selector = cursor;
while (*cursor != 0 && *cursor != ' ' && *cursor != '\t') cursor++;
message.selector_len = static_cast<size_t>(cursor - message.selector);
cursor = skipRecentRepeaterSpaces(cursor);
if (*cursor == 0) return TerminalChannelCommandMatch::MissingMessage;
message.text = cursor;
return TerminalChannelCommandMatch::Valid;
}
inline bool parseTerminalChannelIndex(const TerminalChannelMessage& message,
size_t max_channels,
size_t& channel_index) {
if (message.selector == nullptr || message.selector_len == 0
|| max_channels == 0) {
return false;
}
size_t value = 0;
for (size_t i = 0; i < message.selector_len; i++) {
const char c = message.selector[i];
if (c < '0' || c > '9') return false;
const size_t digit = static_cast<size_t>(c - '0');
if (digit >= max_channels
|| value > (max_channels - 1 - digit) / 10) {
return false;
}
value = value * 10 + digit;
}
if (value >= max_channels) return false;
channel_index = value;
return true;
}
inline bool terminalChannelNameMatches(const TerminalChannelMessage& message,
const char* channel_name) {
return message.selector != nullptr && channel_name != nullptr
&& strlen(channel_name) == message.selector_len
&& memcmp(channel_name, message.selector, message.selector_len) == 0;
}
inline int recentRepeaterHexNibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+174
View File
@@ -0,0 +1,174 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
namespace mesh {
struct RoundTripTracePath {
uint8_t hash_size;
uint8_t hop_count;
size_t byte_len;
};
enum class RawTracePathParseResult : uint8_t {
Valid,
MissingHashSize,
InvalidHashSize,
MissingPrefixes,
InvalidPrefix,
TooManyHops,
RouteTooLong,
};
struct RawTracePath {
uint8_t hash_size;
uint8_t hop_count;
size_t byte_len;
};
inline bool isRawTracePathSeparator(char c) {
return c == ' ' || c == '\t' || c == ',';
}
inline int rawTraceHexNibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
// Parse: <hash-size> <prefix>[,<prefix> ...]
// Prefix separators may be commas, spaces, tabs, or any mixture of them.
// TRACE uses power-of-two hash sizes, so only 1, 2, and 4 are accepted.
inline RawTracePathParseResult parseRawTracePath(const char* input,
uint8_t* output,
size_t output_capacity,
uint8_t max_hops,
RawTracePath& result) {
result.hash_size = 0;
result.hop_count = 0;
result.byte_len = 0;
if (input == nullptr) return RawTracePathParseResult::MissingHashSize;
while (*input == ' ' || *input == '\t') input++;
if (*input == 0) return RawTracePathParseResult::MissingHashSize;
const char width_char = *input++;
if (width_char != '1' && width_char != '2' && width_char != '4') {
return RawTracePathParseResult::InvalidHashSize;
}
if (*input != 0 && !isRawTracePathSeparator(*input)) {
return RawTracePathParseResult::InvalidHashSize;
}
const uint8_t hash_size = static_cast<uint8_t>(width_char - '0');
result.hash_size = hash_size;
while (isRawTracePathSeparator(*input)) input++;
if (*input == 0) return RawTracePathParseResult::MissingPrefixes;
if (output == nullptr || output_capacity < hash_size) {
return RawTracePathParseResult::RouteTooLong;
}
const size_t expected_chars = static_cast<size_t>(hash_size) * 2;
size_t offset = 0;
uint8_t hop_count = 0;
while (*input != 0) {
const char* token = input;
size_t token_len = 0;
while (input[token_len] != 0
&& !isRawTracePathSeparator(input[token_len])) {
token_len++;
}
if (token_len != expected_chars) {
return RawTracePathParseResult::InvalidPrefix;
}
if (hop_count >= max_hops) {
return RawTracePathParseResult::TooManyHops;
}
if (offset + hash_size > output_capacity) {
return RawTracePathParseResult::RouteTooLong;
}
for (uint8_t i = 0; i < hash_size; i++) {
const int high = rawTraceHexNibble(token[i * 2]);
const int low = rawTraceHexNibble(token[i * 2 + 1]);
if (high < 0 || low < 0) {
return RawTracePathParseResult::InvalidPrefix;
}
output[offset++] = static_cast<uint8_t>((high << 4) | low);
}
hop_count++;
input += token_len;
while (isRawTracePathSeparator(*input)) input++;
}
result.hash_size = hash_size;
result.hop_count = hop_count;
result.byte_len = offset;
return RawTracePathParseResult::Valid;
}
// Build the route used by Companion clients for a trace that returns to its
// origin: saved path, optional repeater/room endpoint, then the saved path in
// reverse. Stored three-byte paths are traced with their first two bytes per
// hop because the TRACE flag format has no unambiguous three-byte mode.
inline bool buildRoundTripTracePath(const uint8_t* saved_path,
uint8_t encoded_path_len,
const uint8_t* endpoint_hash,
bool include_endpoint,
uint8_t* output,
size_t output_capacity,
RoundTripTracePath& result) {
result.hash_size = 0;
result.hop_count = 0;
result.byte_len = 0;
const size_t saved_hash_size = (encoded_path_len >> 6) + 1;
const size_t saved_hop_count = encoded_path_len & 63;
if (saved_hash_size == 0 || saved_hash_size > 3
|| (saved_hop_count > 0 && saved_path == nullptr)
|| (include_endpoint && endpoint_hash == nullptr)) {
return false;
}
const size_t trace_hash_size = saved_hash_size == 3 ? 2 : saved_hash_size;
const size_t route_hop_count = saved_hop_count * 2
+ (include_endpoint ? 1 : 0);
if (route_hop_count == 0 || route_hop_count > 255
|| trace_hash_size > output_capacity
|| route_hop_count > output_capacity / trace_hash_size
|| output == nullptr) {
return false;
}
size_t offset = 0;
for (size_t i = 0; i < saved_hop_count; i++) {
memcpy(&output[offset], &saved_path[i * saved_hash_size], trace_hash_size);
offset += trace_hash_size;
}
if (include_endpoint) {
memcpy(&output[offset], endpoint_hash, trace_hash_size);
offset += trace_hash_size;
}
for (size_t i = saved_hop_count; i > 0; i--) {
memcpy(&output[offset], &saved_path[(i - 1) * saved_hash_size],
trace_hash_size);
offset += trace_hash_size;
}
result.hash_size = static_cast<uint8_t>(trace_hash_size);
result.hop_count = static_cast<uint8_t>(route_hop_count);
result.byte_len = offset;
return true;
}
inline uint8_t traceFlagsForHashSize(uint8_t hash_size) {
return hash_size == 1 ? 0
: hash_size == 2 ? 1
: hash_size == 4 ? 2
: 0xFF;
}
} // namespace mesh