// system includes #include #include #include #include // local headers #include "includes/desktopfileentry.h" #include "includes/desktopfile_exceptions.h" #include "includes/desktopfilereader.h" #include "includes/desktopfile_util.h" namespace linuxdeploy { namespace desktopfile { class DesktopFileReader::PrivateData { public: std::string path; DesktopFile::sections_t sections; public: bool isEmpty() { return sections.empty(); } void assertPathIsNotEmpty() { if (path.empty()) throw IOError("empty path is not permitted"); } void copyData(const std::shared_ptr& other) { path = other->path; sections = other->sections; } void parse(std::istream& file) { std::string line; bool first = true; std::string currentSectionName; while (std::getline(file, line)) { if (first) { first = false; // said to allow handling of UTF-16/32 documents, not entirely sure why if (line[0] == static_cast(0xEF)) { line.erase(0, 3); return; } } if (!line.empty()) { auto len = line.length(); if (len > 0 && !((len >= 2 && (line[0] == '/' && line[1] == '/')) || (len >= 1 && line[0] == '#'))) { if (line[0] == '[') { if (line.find_last_of('[') != 0) throw ParseError("Multiple opening [ brackets"); // this line apparently introduces a new section auto closingBracketPos = line.find(']'); auto lastClosingBracketPos = line.find_last_of(']'); if (closingBracketPos == std::string::npos) throw ParseError("No closing ] bracket in section header"); else if (closingBracketPos != lastClosingBracketPos) throw ParseError("Two or more closing ] brackets in section header"); size_t length = len - 2; auto title = line.substr(1, closingBracketPos - 1); // set up the new section sections.insert(std::make_pair(title, DesktopFile::section_t())); currentSectionName = std::move(title); } else { // we require at least one section to be present in the desktop file if (currentSectionName.empty()) throw ParseError("No section in desktop file"); auto delimiterPos = line.find('='); if (delimiterPos == std::string::npos) throw ParseError("No = key/value delimiter found"); // this line should be a normal key-value pair std::string key = line.substr(0, delimiterPos); std::string value = line.substr(delimiterPos + 1, line.size()); // we can strip away any sort of leading or trailing whitespace safely trim(key); trim(value); // empty keys are not allowed for obvious reasons if (key.empty()) throw ParseError("Empty keys are not allowed"); // check if the string is a potentially localized string // if yes, parse name and locale out, and check them for validity std::string entryName, entryLocale; auto openingBracketPos = key.find('['); if (openingBracketPos != std::string::npos) { entryName = key.substr(0, key.find('[')); entryLocale = key.substr(openingBracketPos, key.size()); } else { entryName = key; } // name may only contain A-Za-z- characters according to specification for (const char c : entryName) { if (!( (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c == '-') ) ) { throw ParseError("Key " + key + " contains invalid character " + std::string{c}); } } // validate locale part if (!entryLocale.empty()) { static const auto errorPrefix = "Invalid localization syntax used in key " + key + ": "; if (std::count(entryLocale.begin(), entryLocale.end(), '[') != 1 || std::count(entryLocale.begin(), entryLocale.end(), '[') != 1) { throw ParseError(errorPrefix + "mismatching [] brackets"); } // just for clarification: _this_ should never happen, given how the strings are // split above if (entryLocale.find('[') != 0) { throw ParseError(errorPrefix + "invalid [ position"); } if (entryLocale.find(']') != entryLocale.size()-1) { throw ParseError(errorPrefix + "invalid ] position"); } // the syntax within the brackets is not tested by intention, as some KDE apps // use a locale called "x-test" for some reason // strict validation of the locale part broke all AppImage builds on the KDE binary // factory } auto& section = sections[currentSectionName]; // keys must be unique in the same section if (section.find(key) != section.end()) throw ParseError("Key " + key + " found more than once"); section[key] = DesktopFileEntry(key, value); } } } } } }; DesktopFileReader::DesktopFileReader() : d(new PrivateData) {} DesktopFileReader::DesktopFileReader(std::string path) : DesktopFileReader() { d->path = std::move(path); d->assertPathIsNotEmpty(); std::ifstream ifs(d->path); if (!ifs) throw IOError("could not open file: " + d->path); d->parse(ifs); } DesktopFileReader::DesktopFileReader(std::istream& is) : DesktopFileReader() { d->parse(is); } DesktopFileReader::DesktopFileReader(const DesktopFileReader& other) : DesktopFileReader() { d->copyData(other.d); } DesktopFileReader& DesktopFileReader::operator=(const DesktopFileReader& other) { if (this != &other) { // set up a new instance of PrivateData, and copy data over from other object d.reset(new PrivateData); d->copyData(other.d); } return *this; } DesktopFileReader& DesktopFileReader::operator=(DesktopFileReader&& other) noexcept { if (this != &other) { // move other object's data into this one, and remove reference there d = other.d; other.d = nullptr; } return *this; } bool DesktopFileReader::isEmpty() const { return d->isEmpty(); } bool DesktopFileReader::operator==(const DesktopFileReader& other) const { return d->path == other.d->path && d->sections == other.d->sections; } bool DesktopFileReader::operator!=(const DesktopFileReader& other) const { return !operator==(other); } std::string DesktopFileReader::path() const { return d->path; } DesktopFile::sections_t DesktopFileReader::data() const { return d->sections; } DesktopFile::section_t DesktopFileReader::operator[](const std::string& name) const { auto it = d->sections.find(name); // the map would lazy-initialize a new entry in case the section doesn't exist // therefore explicitly checking whether the section exists, throwing an exception in case it does not if (it == d->sections.end()) throw UnknownSectionError(name); return it->second; } } }