fix(nomadnet): submit configured link variables

This commit is contained in:
torlando-agent[bot]
2026-08-14 18:11:32 +00:00
parent 58db4f1a16
commit b50ae852ac
7 changed files with 118 additions and 10 deletions
+8 -2
View File
@@ -51,7 +51,8 @@ bool CompactPage::assign(const Document& document) {
}
}
for (std::size_t i = 0; i < link_count; ++i) {
const std::size_t bytes = document.links[i].target.size() + 1;
const std::size_t bytes = document.links[i].target.size() +
(document.links[i].fields.empty() ? 0 : document.links[i].fields.size() + 1) + 1;
if (bytes > MAX_ARENA_BYTES - std::min(arena_size, MAX_ARENA_BYTES)) return false;
arena_size += bytes;
}
@@ -62,7 +63,12 @@ bool CompactPage::assign(const Document& document) {
for (std::size_t i = 0; i < link_count; ++i) {
LinkRecord link;
if (!append(document.links[i].target, link.target_offset, link.target_length)) {
std::string navigation_target = document.links[i].target;
if (!document.links[i].fields.empty()) {
navigation_target += '`';
navigation_target += document.links[i].fields;
}
if (!append(navigation_target, link.target_offset, link.target_length)) {
clear();
return false;
}
+57
View File
@@ -2,6 +2,7 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "NomadNetMemory.h"
@@ -13,6 +14,62 @@ inline std::vector<uint8_t> no_form_request_data() {
return {0xc0};
}
inline void append_msgpack_string(std::vector<uint8_t>& output, const std::string& value) {
const std::size_t size = value.size();
if (size <= 31) {
output.push_back(static_cast<uint8_t>(0xa0 | size));
} else if (size <= 0xff) {
output.push_back(0xd9);
output.push_back(static_cast<uint8_t>(size));
} else {
output.push_back(0xda);
output.push_back(static_cast<uint8_t>(size >> 8));
output.push_back(static_cast<uint8_t>(size));
}
output.insert(output.end(), value.begin(), value.end());
}
inline std::vector<uint8_t> request_data(const std::string& fields) {
if (fields.empty()) return no_form_request_data();
std::size_t variable_count = 0;
for (std::size_t start = 0; start <= fields.size();) {
const std::size_t end = fields.find('|', start);
const std::size_t length = (end == std::string::npos ? fields.size() : end) - start;
const std::string field = fields.substr(start, length);
const std::size_t equals = field.find('=');
if (equals != std::string::npos && field.find('=', equals + 1) == std::string::npos) {
++variable_count;
}
if (end == std::string::npos) break;
start = end + 1;
}
std::vector<uint8_t> output;
output.reserve(fields.size() + variable_count * 5 + 3);
if (variable_count <= 15) {
output.push_back(static_cast<uint8_t>(0x80 | variable_count));
} else {
output.push_back(0xde);
output.push_back(static_cast<uint8_t>(variable_count >> 8));
output.push_back(static_cast<uint8_t>(variable_count));
}
for (std::size_t start = 0; start <= fields.size();) {
const std::size_t end = fields.find('|', start);
const std::size_t length = (end == std::string::npos ? fields.size() : end) - start;
const std::string field = fields.substr(start, length);
const std::size_t equals = field.find('=');
if (equals != std::string::npos && field.find('=', equals + 1) == std::string::npos) {
append_msgpack_string(output, "var_" + field.substr(0, equals));
append_msgpack_string(output, field.substr(equals + 1));
}
if (end == std::string::npos) break;
start = end + 1;
}
return output;
}
class ResponseBuffer {
public:
static constexpr std::size_t MAX_BYTES = 64 * 1024;
+15 -4
View File
@@ -18,13 +18,23 @@ bool Url::parse(const std::string& input, Url& result, std::string& error,
return false;
}
}
const auto colon = input.find(':');
if (colon != std::string::npos && input.find(':', colon + 1) != std::string::npos) {
const auto fields_separator = input.find('`');
if (fields_separator != std::string::npos &&
input.find('`', fields_separator + 1) != std::string::npos) {
error = "Address has too many field separators";
return false;
}
const std::string address = fields_separator == std::string::npos
? input : input.substr(0, fields_separator);
std::string fields = fields_separator == std::string::npos
? std::string() : input.substr(fields_separator + 1);
const auto colon = address.find(':');
if (colon != std::string::npos && address.find(':', colon + 1) != std::string::npos) {
error = "Address has too many separators";
return false;
}
std::string destination = colon == std::string::npos ? input : input.substr(0, colon);
std::string path = colon == std::string::npos ? DEFAULT_PATH : input.substr(colon + 1);
std::string destination = colon == std::string::npos ? address : address.substr(0, colon);
std::string path = colon == std::string::npos ? DEFAULT_PATH : address.substr(colon + 1);
if (destination.empty()) destination = current_destination;
if (destination.size() != 32 ||
!std::all_of(destination.begin(), destination.end(), [](unsigned char c) { return std::isxdigit(c); })) {
@@ -44,6 +54,7 @@ bool Url::parse(const std::string& input, Url& result, std::string& error,
}
result.destination_hex = std::move(destination);
result.path = std::move(path);
result.fields = std::move(fields);
return true;
}
+4 -1
View File
@@ -8,8 +8,11 @@ struct Url {
static constexpr const char* DEFAULT_PATH = "/page/index.mu";
std::string destination_hex;
std::string path;
std::string fields;
std::string str() const { return destination_hex + ":" + path; }
std::string str() const {
return destination_hex + ":" + path + (fields.empty() ? std::string() : "`" + fields);
}
static bool parse(const std::string& input, Url& result, std::string& error,
const std::string& current_destination = {});
+2 -2
View File
@@ -2189,10 +2189,10 @@ void UIManager::nomad_send_request() {
if (!_nomad_link || _nomad_link.status() != Type::Link::ACTIVE) return;
nomad_heap_checkpoint("request-enter");
_nomad_link.set_resource_started_callback(on_nomad_resource_started);
const auto nil = NomadNet::no_form_request_data();
const auto request_data = NomadNet::request_data(_nomad_url.fields);
_nomad_request = _nomad_link.request(
Bytes(reinterpret_cast<const uint8_t*>(_nomad_url.path.data()), _nomad_url.path.size()),
Bytes(nil.data(), nil.size()), on_nomad_response, on_nomad_failed,
Bytes(request_data.data(), request_data.size()), on_nomad_response, on_nomad_failed,
on_nomad_progress, 30.0, NomadNet::AsyncMailbox::MAX_WIRE_BYTES);
nomad_heap_checkpoint("request-created");
if (!_nomad_request) {
@@ -133,6 +133,12 @@ int main(int argc, char** argv) {
check("relative same-node path parses with context",
Url::parse(":/page/about.mu", url, error, "fedcba9876543210fedcba9876543210") &&
url.destination_hex == "fedcba9876543210fedcba9876543210");
check("link fields are separated from the registered request path",
Url::parse(":/page/repo.mu`g=reticulum|r=lxmf", url, error,
"fedcba9876543210fedcba9876543210") &&
url.path == "/page/repo.mu" && url.fields == "g=reticulum|r=lxmf");
check("canonical URL preserves link fields for history and reload",
url.str() == "fedcba9876543210fedcba9876543210:/page/repo.mu`g=reticulum|r=lxmf");
check("wrong destination length rejected", !Url::parse("abcd:/page/index.mu", url, error));
check("nonhex destination rejected", !Url::parse("zz23456789abcdef0123456789abcdef:/page/index.mu", url, error));
check("control characters rejected", !Url::parse("0123456789abcdef0123456789abcdef:/page/a\nb", url, error));
@@ -332,6 +338,11 @@ int main(int argc, char** argv) {
auto link_fields = parser.parse("`[Search`:/page/search.mu`q=pyxis]\n");
check("link target excludes request fields", link_fields.links.size() == 1 &&
link_fields.links[0].target == ":/page/search.mu" && link_fields.links[0].fields == "q=pyxis");
CompactPage link_fields_page;
check("compact link navigation retains request fields",
link_fields_page.assign(link_fields) && link_fields_page.links().size() == 1 &&
std::string(link_fields_page.target(0).data(), link_fields_page.target(0).size()) ==
":/page/search.mu`q=pyxis");
check("divider parsed", doc.blocks[3].type == BlockType::DIVIDER);
check("literal mode suppresses formatting", doc.blocks[4].runs.size() == 1 &&
doc.blocks[4].runs[0].text == "`!literal");
@@ -411,17 +422,37 @@ int main(int argc, char** argv) {
check("authoritative Aleph fixture is readable", input.good() || input.eof());
check("authoritative fixture yields headings", !real.blocks.empty() && real.blocks[0].type == BlockType::HEADING);
check("authoritative fixture yields links", !real.links.empty());
bool retained_fixture_fields = false;
CompactPage real_page;
if (real_page.assign(real)) {
for (std::size_t i = 0; i < real.links.size(); ++i) {
if (real.links[i].fields.empty()) continue;
const auto target = real_page.target(i);
retained_fixture_fields = std::string(target.data(), target.size()) ==
real.links[i].target + "`" + real.links[i].fields;
break;
}
}
check("authoritative link fields survive compact navigation", retained_fixture_fields);
check("authoritative fixture remains within bounds", real.blocks.size() <= DocumentParser::MAX_BLOCKS &&
real.links.size() <= DocumentParser::MAX_LINKS);
} else {
check("authoritative Aleph fixture is readable", false);
check("authoritative fixture yields headings", false);
check("authoritative fixture yields links", false);
check("authoritative link fields survive compact navigation", false);
check("authoritative fixture remains within bounds", false);
}
const auto nil = UI::LXMF::NomadNet::no_form_request_data();
check("no-form request is exact msgpack nil", nil.size() == 1 && nil[0] == 0xc0);
const auto configured_variables = UI::LXMF::NomadNet::request_data("g=reticulum|r=lxmf");
const std::vector<uint8_t> expected_variables{
0x82,
0xa5, 'v', 'a', 'r', '_', 'g', 0xa9, 'r', 'e', 't', 'i', 'c', 'u', 'l', 'u', 'm',
0xa5, 'v', 'a', 'r', '_', 'r', 0xa4, 'l', 'x', 'm', 'f'};
check("configured link variables encode as the NomadNet request-data map",
configured_variables == expected_variables);
ResponseBuffer response;
const uint8_t bin8[] = {0xc4, 0x03, 'm', 'u', '!'};
check("msgpack bin8 response normalizes", UI::LXMF::NomadNet::normalize_response(bin8, sizeof(bin8), response) &&
+1 -1
View File
@@ -78,7 +78,7 @@ def test_ui_wiring_contract():
assert "nomadnetwork\", \"node" in manager_cpp
assert "Identity::recall" in manager_cpp
assert "Transport::request_path" in manager_cpp
assert "no_form_request_data" in manager_cpp
assert "request_data(_nomad_url.fields)" in manager_cpp
request_start = manager_cpp.index("void UIManager::nomad_send_request()")
request = manager_cpp[request_start:manager_cpp.index("void UIManager::nomad_update()", request_start)]
assert "30.0, NomadNet::AsyncMailbox::MAX_WIRE_BYTES" in request