FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
libxml_wrapper.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2024-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7/**
8 * @file libxml_wrapper.cpp
9 * @brief Wrapper for managing XML documents and elements using libxml2.
10 */
11
12#include "libxml_wrapper.h"
13
14#include <array>
15#include <cctype>
16#include <cstdarg>
17#include <cstdio>
18#include <format>
19#include <limits>
20#include <string>
21
22#include "libxml/encoding.h"
23#include "libxml/parser.h"
24#include "libxml/valid.h"
25#include "libxml/xmlIO.h"
26#include "libxml/xmlerror.h"
27#include "libxml/xmlschemas.h"
28
29namespace
30{
31 /// Captures libxml2 generic errors into a std::string buffer.
32 void libxmlGenericErrorCallback(void* ctx, const char* msg, ...)
33 {
34 auto* err_str = static_cast<std::string*>(ctx);
35 if ((err_str == nullptr) || (msg == nullptr))
36 {
37 return;
38 }
39
40 std::array<char, 1024> buf{};
42 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): va_list is an array typedef on this ABI.
44 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): va_list is required by vsnprintf.
45 std::vsnprintf(buf.data(), buf.size(), msg, args);
46 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): va_list is an array typedef on this ABI.
47 va_end(args);
48
49 err_str->append(buf.data());
50 }
51
52 [[nodiscard]] int checkedXmlInputSize(const std::size_t size)
53 {
54 if (size > static_cast<std::size_t>(std::numeric_limits<int>::max()))
55 {
56 throw XmlException("XML input exceeds libxml2 length limit.");
57 }
58 return static_cast<int>(size);
59 }
60
61 [[nodiscard]] std::string bytesToString(const std::span<const unsigned char> bytes)
62 {
63 std::string result;
64 result.reserve(bytes.size());
65 for (const unsigned char byte : bytes)
66 {
67 result.push_back(static_cast<char>(byte));
68 }
69 return result;
70 }
71
72 /// Formats a raw XML error message with a title and remediation hint.
73 std::string formatError(const std::string_view title, const std::string& rawErrors, const std::string_view hint)
74 {
75 std::string errors = rawErrors;
76
77 // Strip trailing newlines or whitespace
78 while (!errors.empty() && (std::isspace(static_cast<unsigned char>(errors.back())) != 0))
79 {
80 errors.pop_back();
81 }
82
83 // Indent multiline errors to align nicely within the box
84 size_t pos = 0;
85 while ((pos = errors.find('\n', pos)) != std::string::npos)
86 {
87 errors.replace(pos, 1, "\n│ ");
88 pos += 8;
89 }
90
91 return std::format("\n"
92 "┌─ {} \n"
93 "│\n"
94 "│ Error Details:\n"
95 "│ {}\n"
96 "│\n"
97 "│ Hint: {}\n"
98 "└────────────────────────────────────────────────────────────────────────────",
99 title, errors.empty() ? "Unknown validation error." : errors, hint);
100 }
101
102 /// Retrieves the most recent libxml parser error as a formatted string.
103 std::string getXmlLastErrorFormatted()
104 {
105 const xmlError* err = xmlGetLastError();
106 if ((err != nullptr) && (err->message != nullptr))
107 {
108 std::string msg = err->message;
109 while (!msg.empty() && (std::isspace(static_cast<unsigned char>(msg.back())) != 0))
110 {
111 msg.pop_back();
112 }
113 if (err->line > 0)
114 {
115 return std::format("Line {}: {}", err->line, msg);
116 }
117 return msg;
118 }
119 return "Syntax error or malformed XML.";
120 }
121}
122
123bool XmlDocument::validateWithDtd(const std::span<const unsigned char> dtdData) const
124{
125 const std::string dtd_string = bytesToString(dtdData);
126 xmlDtdPtr dtd =
127 xmlIOParseDTD(nullptr,
131 if (dtd == nullptr)
132 {
133 throw XmlException("Failed to parse DTD from memory.");
134 }
135
136 const std::unique_ptr<xmlValidCtxt, decltype(&xmlFreeValidCtxt)> validation_ctxt(xmlNewValidCtxt(),
138 if (!validation_ctxt)
139 {
141 throw XmlException("Failed to create validation context.");
142 }
143
144 // Bind our custom error handler into the DTD Validation Context
145 std::string dtdErrors;
146 validation_ctxt->userData = &dtdErrors;
149
150 const bool is_valid = xmlValidateDtd(validation_ctxt.get(), _doc.get(), dtd) != 0;
152
153 if (!is_valid)
154 {
155 const std::string fancyError =
156 formatError("XML DTD Validation Failed", dtdErrors,
157 "Check your scenario XML tags and attributes against 'fers-xml.dtd'.");
159 throw XmlException("XML failed DTD validation.");
160 }
161
162 return true;
163}
164
165bool XmlDocument::validateWithXsd(const std::span<const unsigned char> xsdData) const
166{
167 const std::string xsd_string = bytesToString(xsdData);
168 const std::unique_ptr<xmlSchemaParserCtxt, decltype(&xmlSchemaFreeParserCtxt)> schema_parser_ctxt(
171 {
172 throw XmlException("Failed to create schema parser context.");
173 }
174
175 // Bind custom error handler into the Schema Parse Context
176 std::string xsdParseErrors;
179
180 const std::unique_ptr<xmlSchema, decltype(&xmlSchemaFree)> schema(xmlSchemaParse(schema_parser_ctxt.get()),
182 if (!schema)
183 {
184 const std::string fancyError =
185 formatError("XSD Schema Parse Failed", xsdParseErrors, "The internal XSD schema is invalid.");
187 throw XmlException("Failed to parse schema from memory.");
188 }
189
190 const std::unique_ptr<xmlSchemaValidCtxt, decltype(&xmlSchemaFreeValidCtxt)> schema_valid_ctxt(
193 {
194 throw XmlException("Failed to create schema validation context.");
195 }
196
197 // Bind custom error handler into the Schema Validation Context
198 std::string xsdErrors;
200 &xsdErrors);
201
202 if (const bool is_valid = xmlSchemaValidateDoc(schema_valid_ctxt.get(), _doc.get()) == 0; !is_valid)
203 {
204 const std::string fancyError =
205 formatError("XML XSD Validation Failed", xsdErrors,
206 "Check your scenario XML tags and attributes against 'fers-xml.xsd'.");
208 throw XmlException("XML failed XSD validation.");
209 }
210
211 return true;
212}
213
215{
216 const XmlElement main_root = mainDoc.getRootElement();
217 const XmlElement included_root = includedDoc.getRootElement();
218
219 for (xmlNodePtr child = included_root.getNode()->children; child != nullptr; child = child->next)
220 {
221 if (child->type == XML_ELEMENT_NODE)
222 {
224 xmlAddChild(main_root.getNode(), new_node);
225 }
226 }
227}
228
230{
231 const XmlElement root = doc.getRootElement();
232
233 while (true)
234 {
235 if (const XmlElement include_element = root.childElement("include", 0); include_element.isValid())
236 {
238 xmlFreeNode(include_element.getNode());
239 }
240 else
241 {
242 break;
243 }
244 }
245}
246
247bool XmlDocument::loadFile(const std::string_view filename)
248{
250 // Pass NOERROR and NOWARNING to prevent default terminal spam, so we handle it cleanly
251 _doc.reset(xmlReadFile(filename.data(), nullptr, XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
252 if (!_doc)
253 {
254 const std::string fancyError =
255 formatError("XML Parsing Failed", getXmlLastErrorFormatted(), "Ensure the XML file is well-formed.");
257 return false;
258 }
259 return true;
260}
261
262bool XmlDocument::loadString(const std::string& content)
263{
265 _doc.reset(xmlReadMemory(content.c_str(), static_cast<int>(content.length()), "in_memory.xml", nullptr,
267 if (!_doc)
268 {
269 const std::string fancyError =
270 formatError("XML Parsing Failed", getXmlLastErrorFormatted(), "Ensure the XML string is well-formed.");
272 return false;
273 }
274 return true;
275}
276
277std::string XmlDocument::dumpToString() const
278{
279 if (!_doc)
280 {
281 LOG(logging::Level::ERROR, "Document is null; Cannot dump to string");
282 return "";
283 }
284 xmlChar* buffer = nullptr;
285 int size = 0;
286 xmlDocDumpFormatMemory(_doc.get(), &buffer, &size, 1);
287 if (buffer == nullptr)
288 {
289 LOG(logging::Level::ERROR, "Failed to dump XML document to memory buffer.");
290 return "";
291 }
292 const std::string result = xml_detail::toString(buffer, size);
294 return result;
295}
Class for managing XML documents.
bool loadFile(std::string_view filename)
Load an XML file into the document.
bool validateWithDtd(std::span< const unsigned char > dtdData) const
Validate the document using a DTD.
bool validateWithXsd(std::span< const unsigned char > xsdData) const
Validate the document using an XSD schema.
bool loadString(const std::string &content)
Load an XML document from a string in memory.
std::string dumpToString() const
Dumps the document to a string.
Class representing a node in an XML document.
Exception class for handling XML-related errors.
RealType length() const noexcept
Calculates the length (magnitude) of the vector.
void mergeXmlDocuments(const XmlDocument &mainDoc, const XmlDocument &includedDoc)
Merge two XML documents.
void removeIncludeElements(const XmlDocument &doc)
Remove "include" elements from the XML document.
Wrapper for managing XML documents and elements using libxml2.
#define LOG(level,...)
Definition logging.h:19
@ FATAL
Fatal level for severe error events.
@ ERROR
Error level for error events.
std::string toString(const xmlChar *value)
math::Vec3 max