This commit is contained in:
KasyanDiGris 2026-01-21 23:35:41 +11:00 committed by GitHub
commit 6a5cab857d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 112 additions and 40 deletions

View File

@ -4,6 +4,14 @@
#include <cstring> #include <cstring>
OpdsParser::OpdsParser() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
errorOccured = true;
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis());
}
}
OpdsParser::~OpdsParser() { OpdsParser::~OpdsParser() {
if (parser) { if (parser) {
XML_StopParser(parser, XML_FALSE); XML_StopParser(parser, XML_FALSE);
@ -14,13 +22,11 @@ OpdsParser::~OpdsParser() {
} }
} }
bool OpdsParser::parse(const char* xmlData, const size_t length) { size_t OpdsParser::write(uint8_t c) { return write(&c, 1); }
clear();
parser = XML_ParserCreate(nullptr); size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
if (!parser) { if (errorOccured) {
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis()); return length;
return false;
} }
XML_SetUserData(parser, this); XML_SetUserData(parser, this);
@ -28,43 +34,48 @@ bool OpdsParser::parse(const char* xmlData, const size_t length) {
XML_SetCharacterDataHandler(parser, characterData); XML_SetCharacterDataHandler(parser, characterData);
// Parse in chunks to avoid large buffer allocations // Parse in chunks to avoid large buffer allocations
const char* currentPos = xmlData; const char* currentPos = reinterpret_cast<const char*>(xmlData);
size_t remaining = length; size_t remaining = length;
constexpr size_t chunkSize = 1024; constexpr size_t chunkSize = 1024;
while (remaining > 0) { while (remaining > 0) {
void* const buf = XML_GetBuffer(parser, chunkSize); void* const buf = XML_GetBuffer(parser, chunkSize);
if (!buf) { if (!buf) {
errorOccured = true;
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis()); Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis());
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
return false; return length;
} }
const size_t toRead = remaining < chunkSize ? remaining : chunkSize; const size_t toRead = remaining < chunkSize ? remaining : chunkSize;
memcpy(buf, currentPos, toRead); memcpy(buf, currentPos, toRead);
const bool isFinal = (remaining == toRead); if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
if (XML_ParseBuffer(parser, static_cast<int>(toRead), isFinal) == XML_STATUS_ERROR) { errorOccured = true;
Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser))); XML_ErrorString(XML_GetErrorCode(parser)));
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
return false; return length;
} }
currentPos += toRead; currentPos += toRead;
remaining -= toRead; remaining -= toRead;
} }
return length;
// Clean up parser
XML_ParserFree(parser);
parser = nullptr;
Serial.printf("[%lu] [OPDS] Parsed %zu entries\n", millis(), entries.size());
return true;
} }
void OpdsParser::flush() {
if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) {
errorOccured = true;
XML_ParserFree(parser);
parser = nullptr;
}
}
bool OpdsParser::error() const { return errorOccured; }
void OpdsParser::clear() { void OpdsParser::clear() {
entries.clear(); entries.clear();
currentEntry = OpdsEntry{}; currentEntry = OpdsEntry{};

View File

@ -1,4 +1,5 @@
#pragma once #pragma once
#include <Print.h>
#include <expat.h> #include <expat.h>
#include <string> #include <string>
@ -42,28 +43,30 @@ using OpdsBook = OpdsEntry;
* } * }
* } * }
*/ */
class OpdsParser { class OpdsParser final : public Print {
public: public:
OpdsParser() = default; OpdsParser();
~OpdsParser(); ~OpdsParser();
// Disable copy // Disable copy
OpdsParser(const OpdsParser&) = delete; OpdsParser(const OpdsParser&) = delete;
OpdsParser& operator=(const OpdsParser&) = delete; OpdsParser& operator=(const OpdsParser&) = delete;
/** size_t write(uint8_t) override;
* Parse an OPDS XML feed. size_t write(const uint8_t*, size_t) override;
* @param xmlData Pointer to the XML data
* @param length Length of the XML data void flush() override;
* @return true if parsing succeeded, false on error
*/ bool error() const;
bool parse(const char* xmlData, size_t length);
operator bool() { return !error(); }
/** /**
* Get the parsed entries (both navigation and book entries). * Get the parsed entries (both navigation and book entries).
* @return Vector of OpdsEntry entries * @return Vector of OpdsEntry entries
*/ */
const std::vector<OpdsEntry>& getEntries() const { return entries; } const std::vector<OpdsEntry>& getEntries() const& { return entries; }
std::vector<OpdsEntry> getEntries() && { return std::move(entries); }
/** /**
* Get only book entries (legacy compatibility). * Get only book entries (legacy compatibility).
@ -96,4 +99,6 @@ class OpdsParser {
bool inAuthor = false; bool inAuthor = false;
bool inAuthorName = false; bool inAuthorName = false;
bool inId = false; bool inId = false;
bool errorOccured = false;
}; };

View File

@ -0,0 +1,15 @@
#include "OpdsStream.h"
OpdsParserStream::OpdsParserStream(OpdsParser& parser) : parser(parser) {}
int OpdsParserStream::available() { return 0; }
int OpdsParserStream::peek() { abort(); }
int OpdsParserStream::read() { abort(); }
size_t OpdsParserStream::write(uint8_t c) { return parser.write(c); }
size_t OpdsParserStream::write(const uint8_t* buffer, size_t size) { return parser.write(buffer, size); }
OpdsParserStream::~OpdsParserStream() { parser.flush(); }

View File

@ -0,0 +1,23 @@
#pragma once
#include <Stream.h>
#include "OpdsParser.h"
class OpdsParserStream : public Stream {
public:
explicit OpdsParserStream(OpdsParser& parser);
// That functions are not implimented for that stream
int available() override;
int peek() override;
int read() override;
virtual size_t write(uint8_t c) override;
virtual size_t write(const uint8_t* buffer, size_t size) override;
~OpdsParserStream() override;
private:
OpdsParser& parser;
};

View File

@ -2,6 +2,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <OpdsStream.h>
#include <WiFi.h> #include <WiFi.h>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@ -264,23 +265,27 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
std::string url = UrlUtils::buildUrl(serverUrl, path); std::string url = UrlUtils::buildUrl(serverUrl, path);
Serial.printf("[%lu] [OPDS] Fetching: %s\n", millis(), url.c_str()); Serial.printf("[%lu] [OPDS] Fetching: %s\n", millis(), url.c_str());
std::string content; OpdsParser parser;
if (!HttpDownloader::fetchUrl(url, content)) {
state = BrowserState::ERROR; {
errorMessage = "Failed to fetch feed"; OpdsParserStream stream{parser};
updateRequired = true; if (!HttpDownloader::fetchUrl(url, stream)) {
return; state = BrowserState::ERROR;
errorMessage = "Failed to fetch feed";
updateRequired = true;
return;
}
} }
OpdsParser parser; if (!parser) {
if (!parser.parse(content.c_str(), content.size())) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "Failed to parse feed"; errorMessage = "Failed to parse feed";
updateRequired = true; updateRequired = true;
return; return;
} }
entries = parser.getEntries(); entries = std::move(parser).getEntries();
Serial.printf("[%lu] [OPDS] Found %d entries\n", millis(), entries.size());
selectorIndex = 0; selectorIndex = 0;
if (entries.empty()) { if (entries.empty()) {

View File

@ -2,6 +2,7 @@
#include <HTTPClient.h> #include <HTTPClient.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <StreamString.h>
#include <WiFiClient.h> #include <WiFiClient.h>
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
@ -9,7 +10,7 @@
#include "util/UrlUtils.h" #include "util/UrlUtils.h"
bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) { bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
// Use WiFiClientSecure for HTTPS, regular WiFiClient for HTTP // Use WiFiClientSecure for HTTPS, regular WiFiClient for HTTP
std::unique_ptr<WiFiClient> client; std::unique_ptr<WiFiClient> client;
if (UrlUtils::isHttpsUrl(url)) { if (UrlUtils::isHttpsUrl(url)) {
@ -34,10 +35,20 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) {
return false; return false;
} }
outContent = http.getString().c_str(); http.writeToStream(&outContent);
http.end(); http.end();
Serial.printf("[%lu] [HTTP] Fetched %zu bytes\n", millis(), outContent.size()); Serial.printf("[%lu] [HTTP] Fetch success\n", millis());
return true;
}
bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) {
StreamString stream;
if (!fetchUrl(url, stream)) {
return false;
}
outContent = stream.c_str();
return true; return true;
} }

View File

@ -27,6 +27,8 @@ class HttpDownloader {
*/ */
static bool fetchUrl(const std::string& url, std::string& outContent); static bool fetchUrl(const std::string& url, std::string& outContent);
static bool fetchUrl(const std::string& url, Stream& stream);
/** /**
* Download a file to the SD card. * Download a file to the SD card.
* @param url The URL to download * @param url The URL to download