Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
8.2 kB · 222 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223#include "OpdsParser.h"
#include <Logging.h>#include <XmlParserUtils.h>
#include <cstring>
namespace {constexpr size_t ENTRY_STORAGE_CAPACITY = 64;constexpr size_t MAX_ENTRIES = ENTRY_STORAGE_CAPACITY - 2;constexpr size_t MAX_TITLE_CHARS = 160;constexpr size_t MAX_AUTHOR_CHARS = 120;constexpr size_t MAX_ID_CHARS = 128;constexpr size_t MAX_HREF_CHARS = 768;constexpr size_t MAX_SEARCH_TEMPLATE_CHARS = 768;constexpr size_t MAX_PAGE_URL_CHARS = 768;} // namespace
OpdsParser::OpdsParser() { parser = XML_ParserCreate(nullptr); if (!parser) { errorOccured = true; LOG_DBG("OPDS", "Couldn't allocate memory for parser"); return; } entries.reserve(ENTRY_STORAGE_CAPACITY); XML_SetUserData(parser, this); XML_SetElementHandler(parser, startElement, endElement); XML_SetCharacterDataHandler(parser, characterData);}
OpdsParser::~OpdsParser() { destroyXmlParser(parser); }
size_t OpdsParser::write(uint8_t c) { return write(&c, 1); }
size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { if (errorOccured) return length;
const char* currentPos = reinterpret_cast<const char*>(xmlData); size_t remaining = length; constexpr size_t chunkSize = 1024;
while (remaining > 0) { const size_t toRead = remaining < chunkSize ? remaining : chunkSize; void* const buf = XML_GetBuffer(parser, toRead); if (!buf) { errorOccured = true; LOG_DBG("OPDS", "Couldn't allocate memory for buffer"); destroyXmlParser(parser); return length; }
memcpy(buf, currentPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) { errorOccured = true; LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser), XML_ErrorString(XML_GetErrorCode(parser))); destroyXmlParser(parser); return length; } currentPos += toRead; remaining -= toRead; } return length;}
void OpdsParser::flush() { if (errorOccured || !parser) return; if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) { errorOccured = true; destroyXmlParser(parser); }}
bool OpdsParser::error() const { return errorOccured; }
void OpdsParser::clear() { entries.clear(); searchTemplate.clear(); nextPageUrl.clear(); prevPageUrl.clear(); currentEntry = OpdsEntry{}; currentText.clear(); inEntry = inTitle = inAuthor = inAuthorName = inId = false; collectCurrentEntry = false; feedTruncated = false;}
std::vector<OpdsEntry> OpdsParser::getBooks() const { std::vector<OpdsEntry> books; for (const auto& entry : entries) { if (entry.type == OpdsEntryType::BOOK) books.push_back(entry); } return books;}
const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) { for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], name) == 0) return atts[i + 1]; } return nullptr;}
void OpdsParser::assignBounded(std::string& target, const char* value, const size_t maxLen) { if (!value) { target.clear(); return; } target.assign(value, strnlen(value, maxLen));}
void OpdsParser::appendBounded(std::string& target, const char* value, const size_t len, const size_t maxLen) { if (target.size() >= maxLen) return; const size_t remaining = maxLen - target.size(); target.append(value, len < remaining ? len : remaining);}
void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { auto* self = static_cast<OpdsParser*>(userData);
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { self->inEntry = true; self->collectCurrentEntry = self->entries.size() < MAX_ENTRIES; self->feedTruncated = self->feedTruncated || !self->collectCurrentEntry; self->currentEntry = OpdsEntry{}; self->currentText.clear(); self->inTitle = self->inAuthor = self->inAuthorName = self->inId = false; return; }
if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) { const char* href = findAttribute(atts, "href"); if (href) { const char* rel = findAttribute(atts, "rel"); const char* type = findAttribute(atts, "type");
if (rel && strcmp(rel, "search") == 0) { if (strstr(href, "{searchTerms}") != nullptr) { assignBounded(self->searchTemplate, href, MAX_SEARCH_TEMPLATE_CHARS); } } else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) { assignBounded(self->nextPageUrl, href, MAX_PAGE_URL_CHARS); } else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) { assignBounded(self->prevPageUrl, href, MAX_PAGE_URL_CHARS); }
if (self->inEntry && self->collectCurrentEntry) { if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr && strcmp(type, "application/epub+zip") == 0) { // Prefer plain EPUB links over derived formats when multiple // acquisition links are present for one entry. const bool isPlainEpub = strstr(href, ".epub") != nullptr || strstr(href, "/epub/") != nullptr; const bool alreadyHasPlainEpub = self->currentEntry.type == OpdsEntryType::BOOK && (self->currentEntry.href.find(".epub") != std::string::npos || self->currentEntry.href.find("/epub/") != std::string::npos); if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) { self->currentEntry.type = OpdsEntryType::BOOK; assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS); } } else if (type && strstr(type, "application/atom+xml") != nullptr) { if (self->currentEntry.type != OpdsEntryType::BOOK) { self->currentEntry.type = OpdsEntryType::NAVIGATION; assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS); } } } } }
if (!self->inEntry || !self->collectCurrentEntry) return;
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { self->inTitle = true; self->currentText.clear(); } else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { self->inAuthor = true; } else if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { self->inAuthorName = true; self->currentText.clear(); } else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { self->inId = true; self->currentText.clear(); }}
void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) { auto* self = static_cast<OpdsParser*>(userData);
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { if (self->collectCurrentEntry && !self->currentEntry.title.empty() && !self->currentEntry.href.empty()) { self->entries.push_back(self->currentEntry); } self->inEntry = false; self->collectCurrentEntry = false; } else if (self->inEntry) { if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { if (self->inTitle) self->currentEntry.title = self->currentText; self->inTitle = false; } else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { self->inAuthor = false; } else if (self->inAuthorName && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { self->currentEntry.author = self->currentText; self->inAuthorName = false; } else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { if (self->inId) self->currentEntry.id = self->currentText; self->inId = false; } }}
void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) { auto* self = static_cast<OpdsParser*>(userData); if (!self->collectCurrentEntry) return; if (self->inTitle) { appendBounded(self->currentText, s, len, MAX_TITLE_CHARS); } else if (self->inAuthorName) { appendBounded(self->currentText, s, len, MAX_AUTHOR_CHARS); } else if (self->inId) { appendBounded(self->currentText, s, len, MAX_ID_CHARS); }}