#include "OpdsParser.h" #include #include OpdsParser::~OpdsParser() { if (parser) { XML_StopParser(parser, XML_FALSE); XML_SetElementHandler(parser, nullptr, nullptr); XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); parser = nullptr; } } bool OpdsParser::parse(const char* xmlData, const size_t length) { clear(); parser = XML_ParserCreate(nullptr); if (!parser) { Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis()); return false; } XML_SetUserData(parser, this); XML_SetElementHandler(parser, startElement, endElement); XML_SetCharacterDataHandler(parser, characterData); // Parse in chunks to avoid large buffer allocations const char* currentPos = xmlData; size_t remaining = length; constexpr size_t chunkSize = 1024; while (remaining > 0) { void* const buf = XML_GetBuffer(parser, chunkSize); if (!buf) { Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis()); XML_ParserFree(parser); parser = nullptr; return false; } const size_t toRead = remaining < chunkSize ? remaining : chunkSize; memcpy(buf, currentPos, toRead); const bool isFinal = (remaining == toRead); if (XML_ParseBuffer(parser, static_cast(toRead), isFinal) == XML_STATUS_ERROR) { Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), XML_ErrorString(XML_GetErrorCode(parser))); XML_ParserFree(parser); parser = nullptr; return false; } currentPos += toRead; remaining -= toRead; } // Clean up parser XML_ParserFree(parser); parser = nullptr; Serial.printf("[%lu] [OPDS] Parsed %zu entries\n", millis(), entries.size()); return true; } void OpdsParser::clear() { entries.clear(); currentEntry = OpdsEntry{}; currentText.clear(); inEntry = false; inTitle = false; inAuthor = false; inAuthorName = false; inId = false; } std::vector OpdsParser::getBooks() const { std::vector 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 XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { auto* self = static_cast(userData); // Check for entry element (with or without namespace prefix) if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { self->inEntry = true; self->currentEntry = OpdsEntry{}; return; } if (!self->inEntry) return; // Check for title element if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { self->inTitle = true; self->currentText.clear(); return; } // Check for author element if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { self->inAuthor = true; return; } // Check for author name element if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { self->inAuthorName = true; self->currentText.clear(); return; } // Check for id element if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { self->inId = true; self->currentText.clear(); return; } // Check for link element if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) { const char* rel = findAttribute(atts, "rel"); const char* type = findAttribute(atts, "type"); const char* href = findAttribute(atts, "href"); if (href) { // Check for acquisition link with epub type (this is a downloadable book) if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr && strcmp(type, "application/epub+zip") == 0) { self->currentEntry.type = OpdsEntryType::BOOK; self->currentEntry.href = href; } // Check for navigation link (subsection or no rel specified with atom+xml type) else if (type && strstr(type, "application/atom+xml") != nullptr) { // Only set navigation link if we don't already have an epub link if (self->currentEntry.type != OpdsEntryType::BOOK) { self->currentEntry.type = OpdsEntryType::NAVIGATION; self->currentEntry.href = href; } } } } } void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) { auto* self = static_cast(userData); // Check for entry end if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { // Only add entry if it has required fields (title and href) if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) { self->entries.push_back(self->currentEntry); } self->inEntry = false; self->currentEntry = OpdsEntry{}; return; } if (!self->inEntry) return; // Check for title end if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { if (self->inTitle) { self->currentEntry.title = self->currentText; } self->inTitle = false; return; } // Check for author end if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { self->inAuthor = false; return; } // Check for author name end if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { if (self->inAuthorName) { self->currentEntry.author = self->currentText; } self->inAuthorName = false; return; } // Check for id end if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { if (self->inId) { self->currentEntry.id = self->currentText; } self->inId = false; return; } } void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) { auto* self = static_cast(userData); // Only accumulate text when in a text element if (self->inTitle || self->inAuthorName || self->inId) { self->currentText.append(s, len); } }