FERS 1.0.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 <cctype>
15#include <cstdarg>
16#include <format>
17#include <string>
18
19#include "libxml/encoding.h"
20#include "libxml/parser.h"
21#include "libxml/valid.h"
22#include "libxml/xmlIO.h"
23#include "libxml/xmlerror.h"
24#include "libxml/xmlschemas.h"
25
26namespace
27{
28 // Callback to capture libxml2 generic errors into a std::string
29 void libxmlGenericErrorCallback(void* ctx, const char* msg, ...)
30 {
31 auto* err_str = static_cast<std::string*>(ctx);
32 if ((err_str == nullptr) || (msg == nullptr))
33 {
34 return;
35 }
36
37 char buf[1024];
38 va_list args;
39 va_start(args, msg);
40 vsnprintf(buf, sizeof(buf), msg, args);
41 va_end(args);
42
43 err_str->append(buf);
44 }
45
46 // Helper to produce an error box
47 std::string formatError(const std::string_view title, const std::string& rawErrors, const std::string_view hint)
48 {
49 std::string errors = rawErrors;
50
51 // Strip trailing newlines or whitespace
52 while (!errors.empty() && (std::isspace(static_cast<unsigned char>(errors.back())) != 0))
53 {
54 errors.pop_back();
55 }
56
57 // Indent multiline errors to align nicely within the box
58 size_t pos = 0;
59 while ((pos = errors.find('\n', pos)) != std::string::npos)
60 {
61 errors.replace(pos, 1, "\n│ ");
62 pos += 8;
63 }
64
65 return std::format("\n"
66 "┌─ {} \n"
67 "│\n"
68 "│ Error Details:\n"
69 "│ {}\n"
70 "│\n"
71 "│ Hint: {}\n"
72 "└────────────────────────────────────────────────────────────────────────────",
73 title, errors.empty() ? "Unknown validation error." : errors, hint);
74 }
75
76 // Retrieves syntax and malformed document errors nicely
77 std::string getXmlLastErrorFormatted()
78 {
79 const xmlError* err = xmlGetLastError();
80 if ((err != nullptr) && (err->message != nullptr))
81 {
82 std::string msg = err->message;
83 while (!msg.empty() && (std::isspace(static_cast<unsigned char>(msg.back())) != 0))
84 {
85 msg.pop_back();
86 }
87 if (err->line > 0)
88 {
89 return std::format("Line {}: {}", err->line, msg);
90 }
91 return msg;
92 }
93 return "Syntax error or malformed XML.";
94 }
95}
96
97bool XmlDocument::validateWithDtd(const std::span<const unsigned char> dtdData) const
98{
99 xmlDtdPtr dtd =
100 xmlIOParseDTD(nullptr,
101 xmlParserInputBufferCreateMem(reinterpret_cast<const char*>(dtdData.data()),
102 static_cast<int>(dtdData.size()), XML_CHAR_ENCODING_UTF8),
103 XML_CHAR_ENCODING_UTF8);
104 if (dtd == nullptr)
105 {
106 throw XmlException("Failed to parse DTD from memory.");
107 }
108
109 const std::unique_ptr<xmlValidCtxt, decltype(&xmlFreeValidCtxt)> validation_ctxt(xmlNewValidCtxt(),
110 xmlFreeValidCtxt);
111 if (!validation_ctxt)
112 {
113 xmlFreeDtd(dtd);
114 throw XmlException("Failed to create validation context.");
115 }
116
117 // Bind our custom error handler into the DTD Validation Context
118 std::string dtdErrors;
119 validation_ctxt->userData = &dtdErrors;
120 validation_ctxt->error = libxmlGenericErrorCallback;
121 validation_ctxt->warning = libxmlGenericErrorCallback;
122
123 const bool is_valid = xmlValidateDtd(validation_ctxt.get(), _doc.get(), dtd) != 0;
124 xmlFreeDtd(dtd);
125
126 if (!is_valid)
127 {
128 std::string fancyError = formatError("XML DTD Validation Failed", dtdErrors,
129 "Check your scenario XML tags and attributes against 'fers-xml.dtd'.");
130 LOG(logging::Level::ERROR, "{}", fancyError);
131 throw XmlException("XML failed DTD validation.");
132 }
133
134 return true;
135}
136
137bool XmlDocument::validateWithXsd(const std::span<const unsigned char> xsdData) const
138{
139 const std::unique_ptr<xmlSchemaParserCtxt, decltype(&xmlSchemaFreeParserCtxt)> schema_parser_ctxt(
140 xmlSchemaNewMemParserCtxt(reinterpret_cast<const char*>(xsdData.data()), static_cast<int>(xsdData.size())),
141 xmlSchemaFreeParserCtxt);
142 if (!schema_parser_ctxt)
143 {
144 throw XmlException("Failed to create schema parser context.");
145 }
146
147 // Bind custom error handler into the Schema Parse Context
148 std::string xsdParseErrors;
149 xmlSchemaSetParserErrors(schema_parser_ctxt.get(), libxmlGenericErrorCallback, libxmlGenericErrorCallback,
150 &xsdParseErrors);
151
152 const std::unique_ptr<xmlSchema, decltype(&xmlSchemaFree)> schema(xmlSchemaParse(schema_parser_ctxt.get()),
153 xmlSchemaFree);
154 if (!schema)
155 {
156 std::string fancyError =
157 formatError("XSD Schema Parse Failed", xsdParseErrors, "The internal XSD schema is invalid.");
158 LOG(logging::Level::FATAL, "{}", fancyError);
159 throw XmlException("Failed to parse schema from memory.");
160 }
161
162 const std::unique_ptr<xmlSchemaValidCtxt, decltype(&xmlSchemaFreeValidCtxt)> schema_valid_ctxt(
163 xmlSchemaNewValidCtxt(schema.get()), xmlSchemaFreeValidCtxt);
164 if (!schema_valid_ctxt)
165 {
166 throw XmlException("Failed to create schema validation context.");
167 }
168
169 // Bind custom error handler into the Schema Validation Context
170 std::string xsdErrors;
171 xmlSchemaSetValidErrors(schema_valid_ctxt.get(), libxmlGenericErrorCallback, libxmlGenericErrorCallback,
172 &xsdErrors);
173
174 if (const bool is_valid = xmlSchemaValidateDoc(schema_valid_ctxt.get(), _doc.get()) == 0; !is_valid)
175 {
176 std::string fancyError = formatError("XML XSD Validation Failed", xsdErrors,
177 "Check your scenario XML tags and attributes against 'fers-xml.xsd'.");
178 LOG(logging::Level::ERROR, "{}", fancyError);
179 throw XmlException("XML failed XSD validation.");
180 }
181
182 return true;
183}
184
185void mergeXmlDocuments(const XmlDocument& mainDoc, const XmlDocument& includedDoc)
186{
187 const XmlElement main_root = mainDoc.getRootElement();
188 const XmlElement included_root = includedDoc.getRootElement();
189
190 for (xmlNodePtr child = included_root.getNode()->children; child != nullptr; child = child->next)
191 {
192 if (child->type == XML_ELEMENT_NODE)
193 {
194 xmlNodePtr new_node = xmlCopyNode(child, 1);
195 xmlAddChild(main_root.getNode(), new_node);
196 }
197 }
198}
199
201{
202 const XmlElement root = doc.getRootElement();
203
204 while (true)
205 {
206 if (XmlElement include_element = root.childElement("include", 0); include_element.isValid())
207 {
208 xmlUnlinkNode(include_element.getNode());
209 xmlFreeNode(include_element.getNode());
210 }
211 else
212 {
213 break;
214 }
215 }
216}
217
218bool XmlDocument::loadFile(const std::string_view filename)
219{
220 xmlResetLastError();
221 // Pass NOERROR and NOWARNING to prevent default terminal spam, so we handle it cleanly
222 _doc.reset(xmlReadFile(filename.data(), nullptr, XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
223 if (!_doc)
224 {
225 std::string fancyError =
226 formatError("XML Parsing Failed", getXmlLastErrorFormatted(), "Ensure the XML file is well-formed.");
227 LOG(logging::Level::ERROR, "{}", fancyError);
228 return false;
229 }
230 return true;
231}
232
233bool XmlDocument::loadString(const std::string& content)
234{
235 xmlResetLastError();
236 _doc.reset(xmlReadMemory(content.c_str(), static_cast<int>(content.length()), "in_memory.xml", nullptr,
237 XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
238 if (!_doc)
239 {
240 std::string fancyError =
241 formatError("XML Parsing Failed", getXmlLastErrorFormatted(), "Ensure the XML string is well-formed.");
242 LOG(logging::Level::ERROR, "{}", fancyError);
243 return false;
244 }
245 return true;
246}
247
248std::string XmlDocument::dumpToString() const
249{
250 if (!_doc)
251 {
252 LOG(logging::Level::ERROR, "Document is null; Cannot dump to string");
253 return "";
254 }
255 xmlChar* buffer = nullptr;
256 int size = 0;
257 xmlDocDumpFormatMemory(_doc.get(), &buffer, &size, 1);
258 if (buffer == nullptr)
259 {
260 LOG(logging::Level::ERROR, "Failed to dump XML document to memory buffer.");
261 return "";
262 }
263 const std::string result(reinterpret_cast<const char*>(buffer), static_cast<size_t>(size));
264 xmlFree(buffer);
265 return result;
266}
Class for managing XML documents.
XmlElement getRootElement() const
Get the root element of the document.
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.
XmlElement childElement(const std::string_view name="", const unsigned index=0) const noexcept
Retrieve a child element by name and index.
xmlNodePtr getNode() const noexcept
Get the underlying XML node pointer.
Exception class for handling XML-related errors.
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.