#include "NomadNetForm.h" #include #include namespace UI::LXMF::NomadNet { namespace { void wipe(char* bytes, std::size_t size) { volatile char* cursor = bytes; while (size-- != 0) *cursor++ = 0; } struct MapEntry { static constexpr std::size_t MAX_KEY_BYTES = sizeof("field_") - 1 + DocumentParser::MAX_FIELD_NAME_BYTES; std::array key{}; std::array value{}; uint16_t key_length = 0; uint16_t value_length = 0; ~MapEntry() { wipe(value.data(), value.size()); } }; bool same_key(const MapEntry& entry, const char* key, std::size_t key_length) { return entry.key_length == key_length && std::memcmp(entry.key.data(), key, key_length) == 0; } FormEncodeResult upsert(ExternalVector& entries, const char* prefix, std::size_t prefix_length, const char* name, std::size_t name_length, const char* value, std::size_t value_length, bool append_checkbox) { if (name_length > DocumentParser::MAX_FIELD_NAME_BYTES || value_length > DocumentParser::MAX_FIELD_VALUE_BYTES) return FormEncodeResult::VALUE_TOO_LARGE; std::array key{}; const std::size_t key_length = prefix_length + name_length; if (key_length > MapEntry::MAX_KEY_BYTES) return FormEncodeResult::INVALID_SELECTOR; std::memcpy(key.data(), prefix, prefix_length); if (name_length != 0) std::memcpy(key.data() + prefix_length, name, name_length); for (auto& entry : entries) { if (!same_key(entry, key.data(), key_length)) continue; if (append_checkbox && entry.value_length != 0) { if (static_cast(entry.value_length) + 1 + value_length > DocumentParser::MAX_FIELD_VALUE_BYTES) return FormEncodeResult::VALUE_TOO_LARGE; entry.value[entry.value_length++] = ','; if (value_length != 0) std::memcpy(entry.value.data() + entry.value_length, value, value_length); entry.value_length = static_cast(entry.value_length + value_length); entry.value[entry.value_length] = '\0'; } else { wipe(entry.value.data(), entry.value.size()); if (value_length != 0) std::memcpy(entry.value.data(), value, value_length); entry.value_length = static_cast(value_length); } return FormEncodeResult::OK; } if (entries.size() >= FormState::MAX_ENTRIES) return FormEncodeResult::TOO_MANY_ENTRIES; MapEntry entry; if (key_length != 0) std::memcpy(entry.key.data(), key.data(), key_length); entry.key_length = static_cast(key_length); if (value_length != 0) std::memcpy(entry.value.data(), value, value_length); entry.value_length = static_cast(value_length); entries.push_back(std::move(entry)); return FormEncodeResult::OK; } bool append_byte(ExternalVector& output, uint8_t value) { if (output.size() >= FormState::MAX_ENCODED_BYTES) return false; output.push_back(value); return true; } bool append_bytes(ExternalVector& output, const char* value, std::size_t size) { if (size > FormState::MAX_ENCODED_BYTES - std::min(output.size(), FormState::MAX_ENCODED_BYTES)) return false; output.insert(output.end(), value, value + size); return true; } bool append_string(ExternalVector& output, const char* value, std::size_t size) { if (size <= 31) { if (!append_byte(output, static_cast(0xa0u | size))) return false; } else if (size <= 0xff) { if (!append_byte(output, 0xd9) || !append_byte(output, static_cast(size))) return false; } else if (size <= 0xffff) { if (!append_byte(output, 0xda) || !append_byte(output, static_cast(size >> 8)) || !append_byte(output, static_cast(size))) return false; } else return false; return append_bytes(output, value, size); } bool selector_has_name(const std::string& selectors, const FormState::FieldState& field) { for (std::size_t start = 0; start <= selectors.size();) { const std::size_t end = selectors.find('|', start); const std::size_t size = (end == std::string::npos ? selectors.size() : end) - start; const std::size_t equals = selectors.find('=', start); const bool assignment = equals != std::string::npos && equals < start + size; if (!assignment && field.name_equals(selectors.data() + start, size)) return true; if (end == std::string::npos) break; start = end + 1; } return false; } } // namespace bool FormState::FieldState::name_equals(const char* bytes, std::size_t size) const { return bytes && size == name_length && std::memcmp(name.data(), bytes, size) == 0; } bool FormState::assign(const CompactPage& page) { clear(); if (page.fields().size() > DocumentParser::MAX_FIELDS) return false; try { _fields.reserve(page.fields().size()); for (std::size_t i = 0; i < page.fields().size(); ++i) { const auto name = page.field_name(i); const auto value = page.field_value(i); if ((!name.data() && !name.empty()) || (!value.data() && !value.empty()) || name.size() > DocumentParser::MAX_FIELD_NAME_BYTES || value.size() > DocumentParser::MAX_FIELD_VALUE_BYTES) { clear(); return false; } FieldState field; field.id = static_cast(i); field.type = page.fields()[i].type; field.partial_region_index = page.fields()[i].partial_region_index; field.name_length = static_cast(name.size()); field.value_length = static_cast(value.size()); if (!name.empty()) std::memcpy(field.name.data(), name.data(), name.size()); if (!value.empty()) std::memcpy(field.value.data(), value.data(), value.size()); field.checked = page.fields()[i].checked; field.masked = page.fields()[i].masked; if (field.type == FormFieldType::RADIO && field.checked) { for (auto& existing : _fields) { if (existing.type == FormFieldType::RADIO && existing.name_length == field.name_length && (field.name_length == 0 || std::memcmp( existing.name.data(), field.name.data(), field.name_length) == 0)) existing.checked = false; } } _fields.push_back(std::move(field)); } return true; } catch (const std::bad_alloc&) { clear(); return false; } } bool FormState::assign_preserving(const CompactPage& page, const FormState& previous) { if (!assign(page)) return false; auto same_identity = [](const FieldState& left, const FieldState& right) { if (left.type != right.type || left.partial_region_index != right.partial_region_index || left.name_length != right.name_length || (left.name_length != 0 && std::memcmp( left.name.data(), right.name.data(), left.name_length) != 0)) return false; if (left.type != FormFieldType::RADIO && left.type != FormFieldType::CHECKBOX) return true; return left.value_length == right.value_length && (left.value_length == 0 || std::memcmp( left.value.data(), right.value.data(), left.value_length) == 0); }; auto same_radio_group = [](const FieldState& left, const FieldState& right) { return left.type == FormFieldType::RADIO && right.type == FormFieldType::RADIO && left.name_length == right.name_length && (left.name_length == 0 || std::memcmp( left.name.data(), right.name.data(), left.name_length) == 0); }; for (std::size_t index = 0; index < _fields.size(); ++index) { auto& field = _fields[index]; if (field.type == FormFieldType::RADIO) { const FieldState* old_selection = nullptr; std::size_t old_selection_index = previous._fields.size(); for (std::size_t old_index = 0; old_index < previous._fields.size(); ++old_index) { const auto& old = previous._fields[old_index]; if (old.checked && same_radio_group(old, field)) { old_selection = &old; old_selection_index = old_index; break; } } std::size_t selected_occurrence = 0; if (old_selection) { for (std::size_t prior = 0; prior < old_selection_index; ++prior) if (same_identity(previous._fields[prior], *old_selection)) ++selected_occurrence; } const FieldState* surviving_selection = nullptr; if (old_selection) { std::size_t occurrence = 0; for (const auto& current : _fields) { if (!same_identity(*old_selection, current)) continue; if (occurrence++ == selected_occurrence) { surviving_selection = ¤t; break; } } } if (!surviving_selection) continue; field.checked = &field == surviving_selection; continue; } std::size_t occurrence = 0; for (std::size_t prior = 0; prior < index; ++prior) { const auto& candidate = _fields[prior]; if (same_identity(candidate, field)) ++occurrence; } std::size_t seen = 0; const FieldState* matched = nullptr; for (const auto& old : previous._fields) { if (!same_identity(old, field)) continue; if (seen++ == occurrence) { matched = &old; break; } } if (!matched) continue; if (field.type == FormFieldType::TEXT || field.type == FormFieldType::PASSWORD) { wipe(field.value.data(), field.value.size()); if (matched->value_length != 0) std::memcpy(field.value.data(), matched->value.data(), matched->value_length); field.value_length = matched->value_length; } else { field.checked = matched->checked; } } return true; } void FormState::clear() { for (auto& field : _fields) wipe(field.value.data(), field.value.size()); ExternalVector().swap(_fields); } bool FormState::set_value(uint16_t id, const std::string& value) { return set_value(id, value.data(), value.size()); } bool FormState::set_value(uint16_t id, const char* value, std::size_t size) { if (id >= _fields.size() || (!value && size != 0) || size > DocumentParser::MAX_FIELD_VALUE_BYTES) return false; auto& field = _fields[id]; if (field.type != FormFieldType::TEXT && field.type != FormFieldType::PASSWORD) return false; wipe(field.value.data(), field.value.size()); if (size != 0) std::memcpy(field.value.data(), value, size); field.value_length = static_cast(size); return true; } bool FormState::set_checked(uint16_t id, bool checked) { if (id >= _fields.size()) return false; auto& field = _fields[id]; if (field.type != FormFieldType::CHECKBOX && field.type != FormFieldType::RADIO) return false; if (field.type == FormFieldType::RADIO && checked) { for (auto& existing : _fields) if (existing.type == FormFieldType::RADIO && existing.name_length == field.name_length && std::memcmp(existing.name.data(), field.name.data(), field.name_length) == 0) existing.checked = false; } field.checked = checked; return true; } FormEncodeResult FormState::encode(const std::string& selectors, ExternalVector& output) const { clear_encoded_form(output); if (selectors.size() > MAX_SELECTOR_BYTES) return FormEncodeResult::INVALID_SELECTOR; try { if (selectors.empty()) { output.push_back(0x80); return FormEncodeResult::OK; } ExternalVector entries; entries.reserve(std::min(MAX_ENTRIES, _fields.size() + 4)); bool all_fields = false; std::size_t selector_count = 0; for (std::size_t start = 0; start <= selectors.size();) { if (++selector_count > MAX_SELECTORS) return FormEncodeResult::INVALID_SELECTOR; const std::size_t end = selectors.find('|', start); const std::size_t size = (end == std::string::npos ? selectors.size() : end) - start; const char* segment = selectors.data() + start; if (size == 1 && segment[0] == '*') all_fields = true; const void* first_equals_ptr = std::memchr(segment, '=', size); if (first_equals_ptr) { const auto* first_equals = static_cast(first_equals_ptr); const std::size_t name_size = static_cast(first_equals - segment); const std::size_t value_size = size - name_size - 1; if (!std::memchr(first_equals + 1, '=', value_size)) { const auto result = upsert(entries, "var_", 4, segment, name_size, first_equals + 1, value_size, false); if (result != FormEncodeResult::OK) return result; } } if (end == std::string::npos) break; start = end + 1; } for (const auto& field : _fields) { if (!all_fields && !selector_has_name(selectors, field)) continue; if ((field.type == FormFieldType::CHECKBOX || field.type == FormFieldType::RADIO) && !field.checked) continue; const bool append_checkbox = field.type == FormFieldType::CHECKBOX; const auto result = upsert(entries, "field_", 6, field.name.data(), field.name_length, field.value.data(), field.value_length, append_checkbox); if (result != FormEncodeResult::OK) return result; } if (entries.size() <= 15) { if (!append_byte(output, static_cast(0x80u | entries.size()))) return FormEncodeResult::OUTPUT_TOO_LARGE; } else { if (!append_byte(output, 0xde) || !append_byte(output, static_cast(entries.size() >> 8)) || !append_byte(output, static_cast(entries.size()))) return FormEncodeResult::OUTPUT_TOO_LARGE; } for (const auto& entry : entries) { if (!append_string(output, entry.key.data(), entry.key_length) || !append_string(output, entry.value.data(), entry.value_length)) { clear_encoded_form(output); return FormEncodeResult::OUTPUT_TOO_LARGE; } } return FormEncodeResult::OK; } catch (const std::bad_alloc&) { clear_encoded_form(output); return FormEncodeResult::OUTPUT_TOO_LARGE; } } } // namespace UI::LXMF::NomadNet