4 #include <unordered_map>
8 #include "includes/desktopfileentry.h"
9 #include "includes/desktopfile_exceptions.h"
10 #include "includes/desktopfilereader.h"
11 #include "includes/desktopfile_util.h"
13 namespace linuxdeploy {
14 namespace desktopfile {
15 class DesktopFileReader::PrivateData {
18 DesktopFile::sections_t sections;
22 return sections.empty();
25 void assertPathIsNotEmpty() {
27 throw IOError("empty path is not permitted");
30 void copyData(const std::shared_ptr<PrivateData>& other) {
32 sections = other->sections;
35 void parse(std::istream& file) {
39 std::string currentSectionName;
41 while (std::getline(file, line)) {
44 // said to allow handling of UTF-16/32 documents, not entirely sure why
45 if (line[0] == static_cast<std::string::value_type>(0xEF)) {
52 auto len = line.length();
54 !((len >= 2 && (line[0] == '/' && line[1] == '/')) || (len >= 1 && line[0] == '#'))) {
56 if (line.find_last_of('[') != 0)
57 throw ParseError("Multiple opening [ brackets");
59 // this line apparently introduces a new section
60 auto closingBracketPos = line.find(']');
61 auto lastClosingBracketPos = line.find_last_of(']');
63 if (closingBracketPos == std::string::npos)
64 throw ParseError("No closing ] bracket in section header");
65 else if (closingBracketPos != lastClosingBracketPos)
66 throw ParseError("Two or more closing ] brackets in section header");
68 size_t length = len - 2;
69 auto title = line.substr(1, closingBracketPos - 1);
71 // set up the new section
72 sections.insert(std::make_pair(title, DesktopFile::section_t()));
73 currentSectionName = std::move(title);
75 // we require at least one section to be present in the desktop file
76 if (currentSectionName.empty())
77 throw ParseError("No section in desktop file");
79 auto delimiterPos = line.find('=');
80 if (delimiterPos == std::string::npos)
81 throw ParseError("No = key/value delimiter found");
83 // this line should be a normal key-value pair
84 std::string key = line.substr(0, delimiterPos);
85 std::string value = line.substr(delimiterPos + 1, line.size());
87 // we can strip away any sort of leading or trailing whitespace safely
91 // empty keys are not allowed for obvious reasons
93 throw ParseError("Empty keys are not allowed");
95 // check if the string is a potentially localized string
96 // if yes, parse name and locale out, and check them for validity
97 std::string entryName, entryLocale;
99 auto openingBracketPos = key.find('[');
100 if (openingBracketPos != std::string::npos) {
101 entryName = key.substr(0, key.find('['));
102 entryLocale = key.substr(openingBracketPos, key.size());
107 // name may only contain A-Za-z- characters according to specification
108 for (const char c : entryName) {
110 (c >= 'A' && c <= 'Z') ||
111 (c >= 'a' && c <= 'z') ||
112 (c >= '0' && c <= '9') ||
116 throw ParseError("Key " + key + " contains invalid character " + std::string{c});
120 // validate locale part
121 if (!entryLocale.empty()) {
122 static const auto errorPrefix = "Invalid localization syntax used in key " + key + ": ";
124 if (std::count(entryLocale.begin(), entryLocale.end(), '[') != 1 ||
125 std::count(entryLocale.begin(), entryLocale.end(), '[') != 1) {
126 throw ParseError(errorPrefix + "mismatching [] brackets");
129 // just for clarification: _this_ should never happen, given how the strings are
131 if (entryLocale.find('[') != 0) {
132 throw ParseError(errorPrefix + "invalid [ position");
135 if (entryLocale.find(']') != entryLocale.size()-1) {
136 throw ParseError(errorPrefix + "invalid ] position");
139 // the syntax within the brackets is not tested by intention, as some KDE apps
140 // use a locale called "x-test" for some reason
141 // strict validation of the locale part broke all AppImage builds on the KDE binary
145 auto& section = sections[currentSectionName];
147 // keys must be unique in the same section
148 if (section.find(key) != section.end())
149 throw ParseError("Key " + key + " found more than once");
151 section[key] = DesktopFileEntry(key, value);
159 DesktopFileReader::DesktopFileReader() : d(new PrivateData) {}
161 DesktopFileReader::DesktopFileReader(std::string path) : DesktopFileReader() {
162 d->path = std::move(path);
163 d->assertPathIsNotEmpty();
165 std::ifstream ifs(d->path);
167 throw IOError("could not open file: " + d->path);
172 DesktopFileReader::DesktopFileReader(std::istream& is) : DesktopFileReader() {
176 DesktopFileReader::DesktopFileReader(const DesktopFileReader& other) : DesktopFileReader() {
177 d->copyData(other.d);
180 DesktopFileReader& DesktopFileReader::operator=(const DesktopFileReader& other) {
181 if (this != &other) {
182 // set up a new instance of PrivateData, and copy data over from other object
183 d.reset(new PrivateData);
184 d->copyData(other.d);
190 DesktopFileReader& DesktopFileReader::operator=(DesktopFileReader&& other) noexcept {
191 if (this != &other) {
192 // move other object's data into this one, and remove reference there
200 bool DesktopFileReader::isEmpty() const {
204 bool DesktopFileReader::operator==(const DesktopFileReader& other) const {
205 return d->path == other.d->path && d->sections == other.d->sections;
208 bool DesktopFileReader::operator!=(const DesktopFileReader& other) const {
209 return !operator==(other);
212 std::string DesktopFileReader::path() const {
216 DesktopFile::sections_t DesktopFileReader::data() const {
220 DesktopFile::section_t DesktopFileReader::operator[](const std::string& name) const {
221 auto it = d->sections.find(name);
223 // the map would lazy-initialize a new entry in case the section doesn't exist
224 // therefore explicitly checking whether the section exists, throwing an exception in case it does not
225 if (it == d->sections.end())
226 throw UnknownSectionError(name);