FlightGear next
AddonMetadataParser.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: AddonMetadataParser.cxx
3 * SPDX-FileComment: Parser for FlightGear add-on metadata files
4 * SPDX-FileCopyrightText: Copyright (C) 2018 Florent Rougon
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8
9#include <regex>
10#include <string>
11#include <tuple>
12#include <vector>
13
14#include <simgear/debug/logstream.hxx>
15#include <simgear/misc/sg_path.hxx>
16#include <simgear/misc/strutils.hxx>
17#include <simgear/props/props.hxx>
18#include <simgear/props/props_io.hxx>
19
20#include "addon_fwd.hxx"
22#include "AddonVersion.hxx"
23#include "contacts.hxx"
24#include "exceptions.hxx"
25#include "pointer_traits.hxx"
26
27#include <Main/globals.hxx>
28#include <Main/locale.hxx>
29
30namespace strutils = simgear::strutils;
31
32using std::string;
33using std::vector;
34
35namespace flightgear::addons
36{
37
38// Static method
39SGPath
41{
42 return addonPath / "addon-metadata.xml";
43}
44
45static std::string getMaybeLocalized(const std::string& tag, SGPropertyNode* base, SGPropertyNode* lang)
46{
47 if (lang) {
48 auto n = lang->getChild(tag);
49 if (n) {
50 return strutils::strip(n->getStringValue());
51 }
52 }
53
54 auto n = base->getChild(tag);
55 if (n) {
56 return strutils::strip(n->getStringValue());
57 }
58
59 return {};
60}
61
62static SGPropertyNode* getAndCheckLocalizedNode(SGPropertyNode* addonNode,
63 const SGPath& metadataFile)
64{
65 const auto localizedNode = addonNode->getChild("localized");
66 if (!localizedNode) {
67 return nullptr;
68 }
69
70 for (int i = 0; i < localizedNode->nChildren(); ++i) {
71 const auto node = localizedNode->getChild(i);
72 const std::string& name = node->getNameString();
73
74 if (name.find('_') != std::string::npos) {
76 "underscores not allowed in names of children of <localized> "
77 "(in add-on metadata file '" + metadataFile.utf8Str() + "'); "
78 "hyphens should be used, as in 'fr-FR' or 'en-GB'");
79 }
80 }
81
82 return localizedNode;
83}
84
85// Static method
88{
89 SGPath metadataFile = getMetadataFile(addonPath);
90 SGPropertyNode addonRoot;
91 Addon::Metadata metadata;
92
93 if (!metadataFile.exists()) {
95 "unable to find add-on metadata file '" + metadataFile.utf8Str() + "'");
96 }
97
98 try {
99 readProperties(metadataFile, &addonRoot);
100 } catch (const sg_exception &e) {
102 "unable to load add-on metadata file '" + metadataFile.utf8Str() + "': " +
103 e.getFormattedMessage());
104 }
105
106 // Check the 'meta' section
107 SGPropertyNode *metaNode = addonRoot.getChild("meta");
108 if (metaNode == nullptr) {
110 "no /meta node found in add-on metadata file '" +
111 metadataFile.utf8Str() + "'");
112 }
113
114 // Check the file type
115 SGPropertyNode *fileTypeNode = metaNode->getChild("file-type");
116 if (fileTypeNode == nullptr) {
118 "no /meta/file-type node found in add-on metadata file '" +
119 metadataFile.utf8Str() + "'");
120 }
121
122 std::string fileType = fileTypeNode->getStringValue();
123 if (fileType != "FlightGear add-on metadata") {
125 "Invalid /meta/file-type value for add-on metadata file '" +
126 metadataFile.utf8Str() + "': '" + fileType + "' "
127 "(expected 'FlightGear add-on metadata')");
128 }
129
130 // Check the format version
131 SGPropertyNode *fmtVersionNode = metaNode->getChild("format-version");
132 if (fmtVersionNode == nullptr) {
134 "no /meta/format-version node found in add-on metadata file '" +
135 metadataFile.utf8Str() + "'");
136 }
137
138 int formatVersion = fmtVersionNode->getIntValue();
139 if (formatVersion != 1) {
141 "unknown format version in add-on metadata file '" +
142 metadataFile.utf8Str() + "': " + std::to_string(formatVersion));
143 }
144
145 // Now the data we are really interested in
146 SGPropertyNode *addonNode = addonRoot.getChild("addon");
147 if (addonNode == nullptr) {
149 "no /addon node found in add-on metadata file '" +
150 metadataFile.utf8Str() + "'");
151 }
152
153 const auto localizedNode = getAndCheckLocalizedNode(addonNode, metadataFile);
154 SGPropertyNode* langStringsNode = globals->get_locale()->selectLanguageNode(localizedNode);
155
156 SGPropertyNode *idNode = addonNode->getChild("identifier");
157 if (idNode == nullptr) {
159 "no /addon/identifier node found in add-on metadata file '" +
160 metadataFile.utf8Str() + "'");
161 }
162 metadata.id = strutils::strip(idNode->getStringValue());
163
164 // Require a non-empty identifier for the add-on
165 if (metadata.id.empty()) {
167 "empty or whitespace-only value for the /addon/identifier node in "
168 "add-on metadata file '" + metadataFile.utf8Str() + "'");
169 } else if (metadata.id.find('.') == std::string::npos) {
170 SG_LOG(SG_GENERAL, SG_WARN,
171 "Add-on identifier '" << metadata.id << "' does not use reverse DNS "
172 "style (e.g., org.flightgear.addons.MyAddon) in add-on metadata "
173 "file '" << metadataFile.utf8Str() + "'");
174 }
175
176 SGPropertyNode *nameNode = addonNode->getChild("name");
177 if (nameNode == nullptr) {
179 "no /addon/name node found in add-on metadata file '" +
180 metadataFile.utf8Str() + "'");
181 }
182
183 metadata.name = getMaybeLocalized("name", addonNode, langStringsNode);
184
185 // Require a non-empty name for the add-on
186 if (metadata.name.empty()) {
188 "empty or whitespace-only value for the /addon/name node in add-on "
189 "metadata file '" + metadataFile.utf8Str() + "'");
190 }
191
192 SGPropertyNode *versionNode = addonNode->getChild("version");
193 if (versionNode == nullptr) {
195 "no /addon/version node found in add-on metadata file '" +
196 metadataFile.utf8Str() + "'");
197 }
198 metadata.version = AddonVersion{
199 strutils::strip(versionNode->getStringValue())};
200
201 metadata.authors = parseContactsNode<Author>(metadataFile,
202 addonNode->getChild("authors"));
203 metadata.maintainers = parseContactsNode<Maintainer>(
204 metadataFile, addonNode->getChild("maintainers"));
205
206 metadata.shortDescription = getMaybeLocalized("short-description", addonNode, langStringsNode);
207 metadata.longDescription = getMaybeLocalized("long-description", addonNode, langStringsNode);
208
209 std::tie(metadata.licenseDesignation, metadata.licenseFile,
210 metadata.licenseUrl) = parseLicenseNode(addonPath, addonNode);
211
212 SGPropertyNode *tagsNode = addonNode->getChild("tags");
213 if (tagsNode != nullptr) {
214 auto tagNodes = tagsNode->getChildren("tag");
215 for (const auto& node: tagNodes) {
216 metadata.tags.push_back(strutils::strip(node->getStringValue()));
217 }
218 }
219
220 SGPropertyNode *minNode = addonNode->getChild("min-FG-version");
221 if (minNode != nullptr) {
222 metadata.minFGVersionRequired = strutils::strip(minNode->getStringValue());
223 } else {
224 metadata.minFGVersionRequired = std::string();
225 }
226
227 SGPropertyNode *maxNode = addonNode->getChild("max-FG-version");
228 if (maxNode != nullptr) {
229 metadata.maxFGVersionRequired = strutils::strip(maxNode->getStringValue());
230 } else {
231 metadata.maxFGVersionRequired = std::string();
232 }
233
234 metadata.homePage = metadata.downloadUrl = metadata.supportUrl =
235 metadata.codeRepositoryUrl = std::string(); // defaults
236 SGPropertyNode *urlsNode = addonNode->getChild("urls");
237 if (urlsNode != nullptr) {
238 SGPropertyNode *homePageNode = urlsNode->getChild("home-page");
239 if (homePageNode != nullptr) {
240 metadata.homePage = strutils::strip(homePageNode->getStringValue());
241 }
242
243 SGPropertyNode *downloadUrlNode = urlsNode->getChild("download");
244 if (downloadUrlNode != nullptr) {
245 metadata.downloadUrl = strutils::strip(downloadUrlNode->getStringValue());
246 }
247
248 SGPropertyNode *supportUrlNode = urlsNode->getChild("support");
249 if (supportUrlNode != nullptr) {
250 metadata.supportUrl = strutils::strip(supportUrlNode->getStringValue());
251 }
252
253 SGPropertyNode *codeRepoUrlNode = urlsNode->getChild("code-repository");
254 if (codeRepoUrlNode != nullptr) {
255 metadata.codeRepositoryUrl =
256 strutils::strip(codeRepoUrlNode->getStringValue());
257 }
258 }
259
260 SG_LOG(SG_GENERAL, SG_DEBUG,
261 "Parsed add-on metadata file: '" << metadataFile.utf8Str() + "'");
262
263 return metadata;
264}
265
266// Utility function for Addon::MetadataParser::parseContactsNode<>()
267//
268// Read a node such as "name", "email" or "url", child of a contact node (e.g.,
269// of an "author" or "maintainer" node).
270static std::string
271parseContactsNode_readNode(const SGPath& metadataFile,
272 SGPropertyNode* contactNode,
273 std::string subnodeName, bool allowEmpty)
274{
275 SGPropertyNode *node = contactNode->getChild(subnodeName);
276 std::string contents;
277
278 if (node != nullptr) {
279 contents = simgear::strutils::strip(node->getStringValue());
280 }
281
282 if (!allowEmpty && contents.empty()) {
284 "in add-on metadata file '" + metadataFile.utf8Str() + "': "
285 "when the node " + contactNode->getPath(true) + " exists, it must have "
286 "a non-empty '" + subnodeName + "' child node");
287 }
288
289 return contents;
290};
291
292// Static method template (private and only used in this file)
293template <class T>
294vector<typename contact_traits<T>::strong_ref>
295Addon::MetadataParser::parseContactsNode(const SGPath& metadataFile,
296 SGPropertyNode* mainNode)
297{
298 using contactTraits = contact_traits<T>;
299 vector<typename contactTraits::strong_ref> res;
300
301 if (mainNode != nullptr) {
302 auto contactNodes = mainNode->getChildren(contactTraits::xmlNodeName());
303 res.reserve(contactNodes.size());
304
305 for (const auto& contactNode: contactNodes) {
306 std::string name, email, url;
307
308 name = parseContactsNode_readNode(metadataFile, contactNode.get(),
309 "name", false /* allowEmpty */);
310 email = parseContactsNode_readNode(metadataFile, contactNode.get(),
311 "email", true);
312 url = parseContactsNode_readNode(metadataFile, contactNode.get(),
313 "url", true);
314
316 res.push_back(ptr_traits::makeStrongRef(name, email, url));
317 }
318 }
319
320 return res;
321};
322
323// Static method
324std::tuple<std::string, SGPath, std::string>
325Addon::MetadataParser::parseLicenseNode(const SGPath& addonPath,
326 SGPropertyNode* addonNode)
327{
328 SGPath metadataFile = getMetadataFile(addonPath);
329 std::string licenseDesignation;
330 SGPath licenseFile;
331 std::string licenseUrl;
332
333 SGPropertyNode *licenseNode = addonNode->getChild("license");
334 if (licenseNode == nullptr) {
335 return std::tuple<std::string, SGPath, std::string>();
336 }
337
338 SGPropertyNode *licenseDesigNode = licenseNode->getChild("designation");
339 if (licenseDesigNode != nullptr) {
340 licenseDesignation = strutils::strip(licenseDesigNode->getStringValue());
341 }
342
343 SGPropertyNode *licenseFileNode = licenseNode->getChild("file");
344 if (licenseFileNode != nullptr) {
345 // This effectively disallows filenames starting or ending with whitespace
346 std::string licenseFile_s = strutils::strip(licenseFileNode->getStringValue());
347
348 if (!licenseFile_s.empty()) {
349 if (licenseFile_s.find('\\') != std::string::npos) {
350 throw errors::error_loading_metadata_file(
351 "in add-on metadata file '" + metadataFile.utf8Str() + "': the "
352 "value of /addon/license/file contains '\\'; please use '/' "
353 "separators only");
354 }
355
356 if (licenseFile_s.find_first_of("/\\") == 0) {
357 throw errors::error_loading_metadata_file(
358 "in add-on metadata file '" + metadataFile.utf8Str() + "': the "
359 "value of /addon/license/file must be relative to the add-on folder, "
360 "however it starts with '" + licenseFile_s[0] + "'");
361 }
362
363#ifdef HAVE_WORKING_STD_REGEX
364 std::regex winDriveRegexp("([a-zA-Z]:).*");
365 std::smatch results;
366
367 if (std::regex_match(licenseFile_s, results, winDriveRegexp)) {
368 std::string winDrive = results.str(1);
369#else // all this 'else' clause should be removed once we actually require C++11
370 if (licenseFile_s.size() >= 2 &&
371 (('a' <= licenseFile_s[0] && licenseFile_s[0] <= 'z') ||
372 ('A' <= licenseFile_s[0] && licenseFile_s[0] <= 'Z')) &&
373 licenseFile_s[1] == ':') {
374 std::string winDrive = licenseFile_s.substr(0, 2);
375#endif
376 throw errors::error_loading_metadata_file(
377 "in add-on metadata file '" + metadataFile.utf8Str() + "': the "
378 "value of /addon/license/file must be relative to the add-on folder, "
379 "however it starts with a Windows drive letter (" + winDrive + ")");
380 }
381
382 licenseFile = addonPath / licenseFile_s;
383 if ( !(licenseFile.exists() && licenseFile.isFile()) ) {
384 throw errors::error_loading_metadata_file(
385 "in add-on metadata file '" + metadataFile.utf8Str() + "': the "
386 "value of /addon/license/file (pointing to '" + licenseFile.utf8Str() +
387 "') doesn't correspond to an existing file");
388 }
389 } // of if (!licenseFile_s.empty())
390 } // of if (licenseFileNode != nullptr)
391
392 SGPropertyNode *licenseUrlNode = licenseNode->getChild("url");
393 if (licenseUrlNode != nullptr) {
394 licenseUrl = strutils::strip(licenseUrlNode->getStringValue());
395 }
396
397 return std::make_tuple(licenseDesignation, licenseFile, licenseUrl);
398}
399
400} // of namespace flightgear::addons
#define i(x)
static SGPath getMetadataFile(const SGPath &addonPath)
static Addon::Metadata parseMetadataFile(const SGPath &addonPath)
std::vector< MaintainerRef > maintainers
FGGlobals * globals
Definition globals.cxx:142
FlightGear Localization Support.
static std::string parseContactsNode_readNode(const SGPath &metadataFile, SGPropertyNode *contactNode, std::string subnodeName, bool allowEmpty)
static std::string getMaybeLocalized(const std::string &tag, SGPropertyNode *base, SGPropertyNode *lang)
static SGPropertyNode * getAndCheckLocalizedNode(SGPropertyNode *addonNode, const SGPath &metadataFile)
const char * name