#pragma once #include "state_storage_policy.h" #include #include #include #include #include #include #include #include #include namespace nvhttp::state_policy { inline bool valid_uuid(const std::string &value) { if (value.size() != 36) return false; for (std::size_t i = 0; i < value.size(); ++i) { if (i == 8 || i == 13 || i == 18 || i == 23) { if (value[i] != '-') return false; } else if (!std::isxdigit(static_cast(value[i]))) return false; } return true; } inline bool boolean(const nlohmann::json &value) { return value.is_boolean() || (value.is_number_integer() && (value == 0 || value == 1)) || (value.is_string() && (value == "true" || value == "false" || value == "0" || value == "1")); } inline bool permission(const nlohmann::json &value) { if (value.is_number_unsigned()) return value.get() <= std::numeric_limits::max(); if (value.is_number_integer()) return value.get() >= 0 && value.get() <= std::numeric_limits::max(); if (!value.is_string()) return false; const auto &text = value.get_ref(); std::uint32_t parsed; const auto result = std::from_chars(text.data(), text.data() + text.size(), parsed); return result.ec == std::errc {} && result.ptr == text.data() + text.size(); } inline bool normalize_last_seen(nlohmann::json &value) { if (value.is_null()) return true; if (value.is_number_unsigned()) return value.get() <= std::numeric_limits::max(); if (value.is_number_integer()) return true; if (!value.is_string()) return false; const auto &text = value.get_ref(); if (text.empty()) return false; const auto digits = text.front() == '-' ? 1U : 0U; if (digits == text.size() || (text[digits] == '0' && text.size() - digits > 1)) return false; std::int64_t parsed; const auto result = std::from_chars(text.data(), text.data() + text.size(), parsed); if (result.ec != std::errc {} || result.ptr != text.data() + text.size()) return false; // Older property-tree writers persisted timestamps as decimal strings. value = parsed; return true; } // Old property-tree writers encode empty arrays as "". Normalize only those // known containers; preserve every Apollo permission, command and unknown key. inline bool normalize_snapshot(nlohmann::json &tree) { if (!tree.is_object() || !tree.contains("root") || !tree["root"].is_object()) return false; auto &root = tree["root"]; if (!root.contains("uniqueid") || !root["uniqueid"].is_string() || !valid_uuid(root["uniqueid"])) return false; for (const auto key : {"remote_display_layout", "last_notified_version"}) { if (root.contains(key) && !root[key].is_string()) return false; } const auto array = [](nlohmann::json &node) { if (node == "") node = nlohmann::json::array(); return node.is_array(); }; std::unordered_set uuids; std::unordered_set certs; if (root.contains("named_devices")) { auto &devices = root["named_devices"]; if (!array(devices) || devices.size() > 256) return false; for (auto &device : devices) { if (!device.is_object() || !device.contains("uuid") || !device["uuid"].is_string() || !valid_uuid(device["uuid"]) || !uuids.insert(device["uuid"]).second || !device.contains("cert") || !device["cert"].is_string() || device["cert"].get_ref().empty() || device["cert"].get_ref().size() > 65536 || !certs.insert(device["cert"]).second) return false; if (device.contains("perm") && !permission(device["perm"])) return false; if (device.contains("enabled") && !boolean(device["enabled"])) return false; if (device.contains("enabled") && device["enabled"].is_number_integer()) device["enabled"] = device["enabled"] == 1; if (device.contains("last_seen") && !normalize_last_seen(device["last_seen"])) return false; for (const auto key : {"name", "display_mode", "hdr_profile", "output_name_override", "virtual_display_mode", "virtual_display_layout"}) { if (device.contains(key) && !device[key].is_string()) return false; } for (const auto key : {"enable_legacy_ordering", "allow_client_commands", "always_use_virtual_display", "prefer_10bit_sdr"}) { if (device.contains(key) && !device[key].is_null() && !boolean(device[key])) return false; if (device.contains(key) && device[key].is_number_integer()) device[key] = device[key] == 1; } for (const auto key : {"do", "undo"}) { if (!device.contains(key)) continue; if (!array(device[key])) return false; for (auto &command : device[key]) { if (!command.is_object() || !command.contains("cmd") || !command["cmd"].is_string() || (command.contains("elevated") && !boolean(command["elevated"]))) return false; if (command.contains("elevated") && command["elevated"].is_number_integer()) command["elevated"] = command["elevated"] == 1; } } if (device.contains("config_overrides") && device["config_overrides"] == "") device["config_overrides"] = nlohmann::json::object(); if (device.contains("config_overrides") && !device["config_overrides"].is_object()) return false; if (device.contains("config_overrides")) { for (const auto &entry : device["config_overrides"].items()) { if (!entry.key().empty() && !entry.value().is_string()) return false; } } } } if (root.contains("devices")) { if (!array(root["devices"])) return false; std::size_t legacy_count = uuids.size(); for (auto &device : root["devices"]) { if (!device.is_object()) return false; if (!device.contains("certs")) continue; if (!array(device["certs"])) return false; for (const auto &cert : device["certs"]) { if (++legacy_count > 256 || !cert.is_string() || cert.get_ref().empty() || cert.get_ref().size() > 65536 || !certs.insert(cert).second) return false; } } } return true; } // The shared property-tree policy validates structure and certificates, but // erases scalar types. Check the original JSON before choosing a recovery // snapshot so a typed-parser failure cannot bypass a usable backup. inline bool valid_primary_json(const std::string &contents) { try { auto typed = nlohmann::json::parse(contents); if (!typed.is_object()) return false; if (!typed.contains("root") || !typed["root"].is_object() || !typed["root"].contains("uniqueid")) return true; return normalize_snapshot(typed); } catch (...) { return false; } } // Restore only the timestamp type lost by property-tree serialization. The // original JSON validator still rejects malformed timestamp values before // selecting a snapshot; a literal string "null" is not an unset timestamp. inline bool primary_tree_json(const boost::property_tree::ptree &tree, nlohmann::json &typed) { try { std::ostringstream serialized; boost::property_tree::write_json(serialized, tree); typed = nlohmann::json::parse(serialized.str()); const auto root = typed.find("root"); if (root == typed.end() || !root->is_object()) return true; const auto devices = root->find("named_devices"); if (devices == root->end() || !devices->is_array()) return true; for (auto &device : *devices) { if (!device.is_object()) continue; const auto last_seen = device.find("last_seen"); if (last_seen == device.end()) continue; if (*last_seen == "null") *last_seen = nullptr; if (!normalize_last_seen(*last_seen)) return false; } return true; } catch (...) { return false; } } inline bool valid_primary_tree(const boost::property_tree::ptree &tree, bool allow_bootstrap) { if (!statefile::policy::valid_primary_state(tree, allow_bootstrap)) return false; if (!statefile::policy::valid_primary_state(tree, false)) return true; nlohmann::json typed; return primary_tree_json(tree, typed) && normalize_snapshot(typed); } } // namespace nvhttp::state_policy