mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-26 12:39:52 +00:00
fix(nomadnet): submit configured link variables
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {});
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user