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