OGS
PrjProcessing.cpp
Go to the documentation of this file.
1
11#include "PrjProcessing.h"
12
13#include <libxml/parser.h>
14#include <libxml/xmlstring.h>
15#include <spdlog/fmt/bundled/core.h>
16#include <xml_patch.h>
17
18#include <filesystem>
19#include <fstream>
20#include <regex>
21#include <sstream>
22
23#include "DisableFPE.h"
24#include "Error.h"
25#include "FileTools.h"
26#include "Logging.h"
27
28namespace
29{
30std::string iostateToString(std::ios_base::iostate const state)
31{
32 std::string result;
33
34 if (state == std::ios_base::goodbit)
35 {
36 result = "goodbit";
37 }
38 else
39 {
40 if (state & std::ios_base::eofbit)
41 {
42 result += "eofbit ";
43 }
44 if (state & std::ios_base::failbit)
45 {
46 result += "failbit ";
47 }
48 if (state & std::ios_base::badbit)
49 {
50 result += "badbit";
51 }
52 // Remove trailing space if there is one
53 if (!result.empty() && result.back() == ' ')
54 {
55 result.pop_back();
56 }
57 }
58 return result;
59}
60} // namespace
61
62namespace BaseLib
63{
64void traverseIncludes(xmlDoc* doc, xmlNode* node,
65 std::filesystem::path const& prj_dir)
66{
67 xmlNode* cur_node = nullptr;
68 for (cur_node = node; cur_node; cur_node = cur_node->next)
69 {
70 if (cur_node->type != XML_ELEMENT_NODE)
71 {
72 continue;
73 }
74 if (xmlStrEqual(cur_node->name, xmlCharStrdup("include")))
75 {
76 auto include_file_char_pointer =
77 xmlGetProp(cur_node, xmlCharStrdup("file"));
78 if (include_file_char_pointer == nullptr)
79 {
81 "Error while processing includes in prj file. Error in "
82 "element '{:s}' on line {:d}: no file attribute given!",
83 reinterpret_cast<const char*>(cur_node->name),
84 cur_node->line);
85 }
86 auto filename_length = xmlStrlen(include_file_char_pointer);
87 std::string filename(
88 reinterpret_cast<char*>(include_file_char_pointer),
89 filename_length);
90 if (auto const filepath = std::filesystem::path(filename);
91 filepath.is_relative())
92 {
93 filename = (prj_dir / filepath).string();
94 }
95
96 if (!std::filesystem::exists(filename))
97 {
99 "Error while processing includes in prj file. Error in "
100 "element '{:s}' on line {:d}: Include file is not "
101 "existing: "
102 "{:s}!",
103 reinterpret_cast<const char*>(cur_node->name),
104 cur_node->line,
105 reinterpret_cast<const char*>(include_file_char_pointer));
106 }
107 INFO("Including {:s} into project file.", filename);
108
109 const std::ifstream input_stream(filename, std::ios_base::binary);
110 if (input_stream.fail())
111 {
112 OGS_FATAL("Failed to open file {}!", filename);
113 }
114 std::stringstream buffer;
115 buffer << input_stream.rdbuf();
116 const std::string xml = buffer.str();
117
118 // Replace lines containing <?xml ... ?>
119 std::regex xmlDeclaration("<\\?xml.*?>");
120 const std::string xml_filtered =
121 std::regex_replace(xml, xmlDeclaration, "");
122
123 xmlNodePtr pNewNode = nullptr;
124 xmlParseInNodeContext(cur_node->parent, xml_filtered.c_str(),
125 (int)xml_filtered.length(), 0, &pNewNode);
126 if (pNewNode != nullptr)
127 {
128 // add new xml node to parent
129 xmlNode* pChild = pNewNode;
130 while (pChild != nullptr)
131 {
132 xmlAddChild(cur_node->parent, xmlCopyNode(pChild, 1));
133 pChild = pChild->next;
134 }
135 xmlFreeNode(pNewNode);
136 }
137
138 // Cleanup and continue on next node
139 auto next_node = cur_node->next;
140 xmlUnlinkNode(cur_node);
141 xmlFreeNode(cur_node);
142 cur_node = next_node;
143 }
144 traverseIncludes(doc, cur_node->children, prj_dir);
145 }
146 xmlFreeNode(cur_node);
147}
148
149void replaceIncludes(std::stringstream& prj_stream,
150 std::filesystem::path const& prj_dir)
151{
152 // Parsing the XML triggers floating point exceptions. Because we are not
153 // debugging libxml2 (or other libraries) at this point, the floating point
154 // exceptions are temporarily disabled and are restored at the end of the
155 // function.
156 [[maybe_unused]] DisableFPE disable_fpe;
157
158 auto doc = xmlReadMemory(prj_stream.str().c_str(), prj_stream.str().size(),
159 nullptr, nullptr, 0);
160 if (doc == nullptr)
161 {
162 OGS_FATAL("Error reading project file from memory.");
163 }
164
165 auto root_node = xmlDocGetRootElement(doc);
166 traverseIncludes(doc, root_node, prj_dir);
167
168 xmlChar* xmlbuff;
169 int buffersize;
170 xmlDocDumpMemory(doc, &xmlbuff, &buffersize);
171 prj_stream.str(""); // Empty stream
172 prj_stream << xmlbuff;
173
174 xmlFree(xmlbuff);
175 xmlFreeDoc(doc);
176}
177
178// Applies a patch file to the prj content in prj_stream.
179void patchStream(std::string const& patch_file, std::stringstream& prj_stream,
180 bool after_includes = false)
181{
182 // xmlReadFile(patch_file.c_str(), nullptr, 0); leads to malloc errors in
183 // xmlFreeDoc(doc) below
184 auto patch = xmlParseFile(patch_file.c_str());
185 if (patch == nullptr)
186 {
187 OGS_FATAL("Error reading XML diff file {:s}.", patch_file);
188 }
189
190 auto doc = xmlReadMemory(prj_stream.str().c_str(), prj_stream.str().size(),
191 nullptr, nullptr, 0);
192 if (doc == nullptr)
193 {
194 OGS_FATAL("Error reading project file from memory.");
195 }
196
197 auto node = xmlDocGetRootElement(patch);
198 int rc = 0;
199 for (node = node ? node->children : nullptr; node; node = node->next)
200 {
201 if (node->type != XML_ELEMENT_NODE)
202 {
203 continue;
204 }
205 bool node_after_includes = false;
206 xmlAttr* attribute = node->properties;
207 while (attribute)
208 {
209 // Check for after_includes-attribute
210 xmlChar* value =
211 xmlNodeListGetString(node->doc, attribute->children, 1);
212 if (xmlStrEqual(attribute->name, xmlCharStrdup("after_includes")) &&
213 xmlStrEqual(value, xmlCharStrdup("true")))
214 {
215 node_after_includes = true;
216 }
217
218 xmlFree(value);
219 attribute = attribute->next;
220 }
221
222 if (after_includes != node_after_includes)
223 {
224 continue;
225 }
226
227 if (xmlStrEqual(node->name, xmlCharStrdup("add")))
228 {
229 rc = xml_patch_add(doc, node);
230 }
231 else if (xmlStrEqual(node->name, xmlCharStrdup("replace")))
232 {
233 rc = xml_patch_replace(doc, node);
234 }
235 else if (xmlStrEqual(node->name, xmlCharStrdup("remove")))
236 {
237 rc = xml_patch_remove(doc, node);
238 }
239 else
240 {
241 OGS_FATAL(
242 "Error while patching prj file with patch file {:}. Only "
243 "'add', 'replace' and 'remove' elements are allowed! Got an "
244 "element '{:s}' on line {:d}.",
245 patch_file, reinterpret_cast<const char*>(node->name),
246 node->line);
247 }
248
249 if (rc)
250 {
251 OGS_FATAL(
252 "Error while patching prj file with patch file {:}. Error in "
253 "element '{:s}' on line {:d}.",
254 patch_file, reinterpret_cast<const char*>(node->name),
255 node->line);
256 }
257 }
258
259 xmlChar* xmlbuff;
260 int buffersize;
261 xmlDocDumpMemory(doc, &xmlbuff, &buffersize);
262 prj_stream.str(""); // Empty stream
263 prj_stream << xmlbuff;
264
265 xmlFree(xmlbuff);
266 xmlFreeDoc(doc);
267 xmlFreeDoc(patch);
268}
269
270// Will set prj_file to the actual .prj file and returns the final prj file
271// content in prj_stream.
272void readAndPatchPrj(std::stringstream& prj_stream, std::string& prj_file,
273 std::vector<std::string>& patch_files)
274{
275 // Extract base project file path if an xml (patch) file is given as prj
276 // file and it contains the base_file attribute.
277 if (BaseLib::getFileExtension(prj_file) == ".xml")
278 {
279 if (!patch_files.empty())
280 {
281 OGS_FATAL(
282 "It is not allowed to specify additional patch files "
283 "if a patch file was already specified as the "
284 "prj-file.");
285 }
286 auto patch = xmlReadFile(prj_file.c_str(), nullptr, 0);
287 auto node = xmlDocGetRootElement(patch);
288 xmlChar const base_file_string[] = "base_file";
289 auto base_file = xmlGetProp(node, base_file_string);
290 if (base_file == nullptr)
291 {
292 OGS_FATAL(
293 "Error reading base prj file (base_file attribute) in given "
294 "patch file {:s}.",
295 prj_file);
296 }
297 patch_files = {prj_file};
298 std::stringstream ss;
299 ss << base_file;
300 prj_file = BaseLib::joinPaths(BaseLib::extractPath(prj_file), ss.str());
301 }
302
303 // read base prj file into stream
304 if (std::ifstream file(prj_file); file)
305 {
306 prj_stream << file.rdbuf();
307 }
308 else
309 {
310 if (!BaseLib::IsFileExisting(prj_file))
311 {
312 ERR("File {:s} does not exist.", prj_file);
313 }
314 DBUG("Stream state flags: {:s}.", iostateToString(file.rdstate()));
315 OGS_FATAL("Could not open project file '{:s}' for reading.", prj_file);
316 }
317
318 // apply xml patches to stream
319 for (const auto& patch_file : patch_files)
320 {
321 patchStream(patch_file, prj_stream);
322 }
323}
324
325void prepareProjectFile(std::stringstream& prj_stream,
326 const std::string& filepath,
327 const std::vector<std::string>& patch_files,
328 bool write_prj, const std::string& out_directory)
329{
330 std::string prj_file = filepath;
331
332 std::vector<std::string> patch_files_copy = patch_files;
333 readAndPatchPrj(prj_stream, prj_file, patch_files_copy);
334 replaceIncludes(prj_stream,
335 std::filesystem::absolute(std::filesystem::path(prj_file))
336 .parent_path());
337 // re-apply xml patches to stream
338 for (const auto& patch_file : patch_files_copy)
339 {
340 patchStream(patch_file, prj_stream, true);
341 }
342
343 if (write_prj)
344 {
345 // The following two lines should set indenting to 4 spaces but it does
346 // not work. 2 spaces are default.
347 //
348 // xmlThrDefIndentTreeOutput(1);
349 // xmlThrDefTreeIndentString(" "); // 4 spaces indent
350 // XML_PARSE_NOBLANKS -> pretty-print
351 auto doc =
352 xmlReadMemory(prj_stream.str().c_str(), prj_stream.str().size(),
353 nullptr, nullptr, XML_PARSE_NOBLANKS);
354 auto prj_out = (std::filesystem::path(out_directory) /
355 std::filesystem::path(filepath).stem())
356 .string() +
357 "_processed.prj";
358 xmlSaveFormatFileEnc(prj_out.c_str(), doc, "utf-8", 1);
359 INFO("Processed project file written to {:s}.", prj_out);
360 xmlFreeDoc(doc);
361 }
362 xmlCleanupParser();
363}
364} // namespace BaseLib
#define OGS_FATAL(...)
Definition Error.h:26
Filename manipulation routines.
void INFO(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:35
void DBUG(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:30
void ERR(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:45
void prepareProjectFile(std::stringstream &prj_stream, const std::string &filepath, const std::vector< std::string > &patch_files, bool write_prj, const std::string &out_directory)
Applies includes and patch files to project file.
void traverseIncludes(xmlDoc *doc, xmlNode *node, std::filesystem::path const &prj_dir)
void readAndPatchPrj(std::stringstream &prj_stream, std::string &prj_file, std::vector< std::string > &patch_files)
void patchStream(std::string const &patch_file, std::stringstream &prj_stream, bool after_includes=false)
std::string getFileExtension(const std::string &path)
std::string extractPath(std::string const &pathname)
bool IsFileExisting(const std::string &strFilename)
Returns true if given file exists.
Definition FileTools.cpp:47
std::string joinPaths(std::string const &pathA, std::string const &pathB)
void replaceIncludes(std::stringstream &prj_stream, std::filesystem::path const &prj_dir)
std::string iostateToString(std::ios_base::iostate const state)