#include "epub.h" #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Lettore ZIP minimale (solo STORE e DEFLATE, niente crittografia) // --------------------------------------------------------------------------- namespace { struct ZipEntry { QString name; quint16 method; quint32 compSize; quint32 uncompSize; quint32 localOffset; }; quint16 le16(const QByteArray &d, int off) { return (quint8)d.at(off) | ((quint8)d.at(off + 1) << 8); } quint32 le32(const QByteArray &d, int off) { return (quint32)(quint8)d.at(off) | ((quint32)(quint8)d.at(off + 1) << 8) | ((quint32)(quint8)d.at(off + 2) << 16) | ((quint32)(quint8)d.at(off + 3) << 24); } bool readZipEntries(const QByteArray &data, QList *entries) { // EOCD: cerca la firma PK\x05\x06 negli ultimi 64 KB + commento const int eocdMax = qMin(data.size(), 65557); int eocd = -1; for (int i = data.size() - eocdMax; i <= data.size() - 22; ++i) { if (data.at(i) == 'P' && data.at(i + 1) == 'K' && data.at(i + 2) == '\x05' && data.at(i + 3) == '\x06') { eocd = i; break; } } if (eocd < 0) return false; const quint32 cdOffset = le32(data, eocd + 16); const quint16 cdCount = le16(data, eocd + 10); int off = cdOffset; for (int i = 0; i < cdCount; ++i) { if (off + 46 > data.size() || data.at(off) != 'P' || data.at(off + 1) != 'K' || data.at(off + 2) != '\x01' || data.at(off + 3) != '\x02') return false; ZipEntry e; e.method = le16(data, off + 10); e.compSize = le32(data, off + 20); e.uncompSize = le32(data, off + 24); const quint16 nameLen = le16(data, off + 28); const quint16 extraLen = le16(data, off + 30); const quint16 commentLen = le16(data, off + 32); e.localOffset = le32(data, off + 42); e.name = QString::fromUtf8(data.mid(off + 46, nameLen)); entries->append(e); off += 46 + nameLen + extraLen + commentLen; } return true; } QByteArray extractEntryData(const QByteArray &data, const ZipEntry &e) { if ((int)e.localOffset + 30 > data.size()) return QByteArray(); const int lo = e.localOffset; if (data.at(lo) != 'P' || data.at(lo + 1) != 'K' || data.at(lo + 2) != '\x03' || data.at(lo + 3) != '\x04') return QByteArray(); const quint16 nameLen = le16(data, lo + 26); const quint16 extraLen = le16(data, lo + 28); const int dataOff = lo + 30 + nameLen + extraLen; if (dataOff + (int)e.compSize > data.size()) return QByteArray(); const QByteArray raw = data.mid(dataOff, e.compSize); if (e.method == 0) // STORE return raw; if (e.method == 8) { // DEFLATE (raw, senza header zlib) QByteArray out; out.resize(e.uncompSize); z_stream zs; std::memset(&zs, 0, sizeof(zs)); if (inflateInit2(&zs, -15) != Z_OK) return QByteArray(); zs.next_in = reinterpret_cast(const_cast(raw.constData())); zs.avail_in = raw.size(); zs.next_out = reinterpret_cast(out.data()); zs.avail_out = out.size(); const int r = inflate(&zs, Z_FINISH); inflateEnd(&zs); if (r != Z_STREAM_END) return QByteArray(); out.resize(zs.total_out); return out; } return QByteArray(); // metodo non supportato } QString readTextFile(const QString &path) { QFile f(path); if (!f.open(QIODevice::ReadOnly)) return QString(); return QString::fromUtf8(f.readAll()); } } // namespace // --------------------------------------------------------------------------- // EpubBook // --------------------------------------------------------------------------- EpubBook::EpubBook() { } EpubBook::~EpubBook() { close(); } void EpubBook::close() { if (!m_baseDir.isEmpty()) { QDir(m_baseDir).removeRecursively(); m_baseDir.clear(); } m_chapters.clear(); m_title.clear(); m_author.clear(); } bool EpubBook::open(const QString &epubPath) { close(); m_opfDir.clear(); QFile f(epubPath); if (!f.open(QIODevice::ReadOnly)) return false; const QByteArray data = f.readAll(); f.close(); QList entries; if (!readZipEntries(data, &entries)) return false; m_baseDir = QDir::tempPath() + QStringLiteral("/harbour-calibreweb/books/") + QString::number(qHash(epubPath), 16); if (!QDir().mkpath(m_baseDir)) return false; // estrai tutto (skip directory e path traversal) for (const ZipEntry &e : entries) { if (e.name.endsWith(QLatin1Char('/')) || e.name.contains(QStringLiteral(".."))) continue; const QByteArray content = extractEntryData(data, e); if (content.isNull()) continue; const QString dest = m_baseDir + QLatin1Char('/') + e.name; QDir().mkpath(QFileInfo(dest).absolutePath()); QFile out(dest); if (out.open(QIODevice::WriteOnly)) { out.write(content); out.close(); } } if (!parseContainer()) return false; if (!parseOpf()) return false; return !m_chapters.isEmpty(); } bool EpubBook::parseContainer() { QFile f(m_baseDir + QStringLiteral("/META-INF/container.xml")); if (!f.open(QIODevice::ReadOnly)) return false; QXmlStreamReader r(&f); while (!r.atEnd() && !r.hasError()) { if (r.readNextStartElement() && r.name() == QLatin1String("rootfile")) { const QString fullPath = r.attributes().value(QLatin1String("full-path")).toString(); if (!fullPath.isEmpty()) { // path RELATIVO alla root epub, non assoluto rispetto alla CWD m_opfDir = QFileInfo(fullPath).path(); if (m_opfDir == QStringLiteral(".")) m_opfDir.clear(); return true; } } } return false; } QString EpubBook::absolutePath(const QString &relativeToOpfDir) const { QString rel = relativeToOpfDir; rel.replace(QLatin1Char('\\'), QLatin1Char('/')); if (rel.contains(QLatin1String(".."))) return QString(); if (!m_opfDir.isEmpty()) rel = m_opfDir + QLatin1Char('/') + rel; return QDir(m_baseDir).filePath(rel); } bool EpubBook::parseOpf() { QFile f(absolutePath(QStringLiteral("content.opf"))); if (!f.exists()) f.setFileName(absolutePath(QStringLiteral("package.opf"))); if (!f.open(QIODevice::ReadOnly)) return false; QXmlStreamReader r(&f); // manifest: id -> {href, media-type, properties} QHash manifestHref; QHash manifestType; QHash manifestProps; // spine: idrefs in ordine QStringList spine; QString navItemId; QString ncxItemId; while (!r.atEnd() && !r.hasError()) { const QXmlStreamReader::TokenType t = r.readNext(); if (t != QXmlStreamReader::StartElement) continue; const QStringRef name = r.name(); const QXmlStreamAttributes a = r.attributes(); if (name == QLatin1String("title") && m_title.isEmpty()) { m_title = r.readElementText().trimmed(); } else if (name == QLatin1String("creator") && m_author.isEmpty()) { m_author = r.readElementText().trimmed(); } else if (name == QLatin1String("item")) { const QString id = a.value(QLatin1String("id")).toString(); manifestHref.insert(id, a.value(QLatin1String("href")).toString()); manifestType.insert(id, a.value(QLatin1String("media-type")).toString()); const QString props = a.value(QLatin1String("properties")).toString(); manifestProps.insert(id, props); if (props.contains(QLatin1String("nav"))) navItemId = id; if (a.value(QLatin1String("media-type")).toString() == QLatin1String("application/x-dtbncx+xml")) ncxItemId = id; } else if (name == QLatin1String("itemref")) { spine.append(a.value(QLatin1String("idref")).toString()); } } if (spine.isEmpty() || manifestHref.isEmpty()) return false; // TOC: EPUB3 nav oppure EPUB2 NCX if (!navItemId.isEmpty()) parseTocEpub3(absolutePath(manifestHref.value(navItemId))); else if (!ncxItemId.isEmpty()) parseTocEpub2(absolutePath(manifestHref.value(ncxItemId))); // mappa path assoluto -> titolo dal TOC QHash titleByPath; for (const EpubChapter &c : m_chapters) titleByPath.insert(c.path, c.title); m_chapters.clear(); // costruisci i capitoli nell'ordine dello spine int n = 0; for (const QString &idref : spine) { const QString href = manifestHref.value(idref); if (href.isEmpty()) continue; const QString abs = absolutePath(href); if (abs.isEmpty() || !QFile::exists(abs)) continue; EpubChapter c; c.id = idref; c.path = abs; c.title = titleByPath.value(abs); if (c.title.isEmpty()) c.title = QStringLiteral("Capitolo ") + QString::number(n + 1); m_chapters.append(c); ++n; } return true; } void EpubBook::parseTocEpub3(const QString &navPath) { QFile f(navPath); if (!f.open(QIODevice::ReadOnly)) return; QXmlStreamReader r(&f); bool inTocNav = false; int depth = 0; while (!r.atEnd() && !r.hasError()) { const QXmlStreamReader::TokenType t = r.readNext(); if (t == QXmlStreamReader::StartElement) { const QStringRef name = r.name(); if (name == QLatin1String("nav")) { if (!inTocNav) { const QString type = r.attributes() .value(QLatin1String("http://www.idpf.org/2007/ops"), QLatin1String("type")).toString(); if (type != QLatin1String("toc")) continue; inTocNav = true; } ++depth; continue; } if (!inTocNav || depth == 0) continue; if (name == QLatin1String("a")) { const QString href = r.attributes().value(QLatin1String("href")).toString(); const QString text = r.readElementText().trimmed(); if (!href.isEmpty() && !text.isEmpty()) { EpubChapter c; c.path = absolutePath(href); c.title = text; m_chapters.append(c); } } } else if (t == QXmlStreamReader::EndElement && r.name() == QLatin1String("nav")) { if (inTocNav && --depth <= 0) break; } } } void EpubBook::parseTocEpub2(const QString &ncxPath) { QFile f(ncxPath); if (!f.open(QIODevice::ReadOnly)) return; QXmlStreamReader r(&f); EpubChapter pending; bool havePending = false; while (!r.atEnd() && !r.hasError()) { const QXmlStreamReader::TokenType t = r.readNext(); if (t == QXmlStreamReader::StartElement) { const QStringRef name = r.name(); if (name == QLatin1String("navPoint")) { // nel NCX il (titolo) viene PRIMA del (src): // accumula in pending e chiudi alla fine del navPoint pending = EpubChapter(); havePending = true; } else if (name == QLatin1String("text")) { if (havePending) pending.title = r.readElementText().trimmed(); } else if (name == QLatin1String("content")) { const QString src = r.attributes().value(QLatin1String("src")).toString(); if (havePending) pending.path = absolutePath(src); } } else if (t == QXmlStreamReader::EndElement && r.name() == QLatin1String("navPoint")) { if (havePending && !pending.path.isEmpty()) m_chapters.append(pending); havePending = false; } } } QString EpubBook::chapterFilePath(int index) const { if (index < 0 || index >= m_chapters.size()) return QString(); return m_chapters.at(index).path; } QString EpubBook::chapterTitle(int index) const { if (index < 0 || index >= m_chapters.size()) return QString(); return m_chapters.at(index).title; } QString EpubBook::makeBookmarkKey(int chapterIndex, double fraction) const { return QStringLiteral("cw:v1:%1:%2").arg(chapterIndex).arg(fraction, 0, 'f', 4); } bool EpubBook::parseBookmarkKey(const QString &key, int *chapterIndex, double *fraction) const { const QStringList parts = key.split(QLatin1Char(':')); if (parts.size() != 4 || parts.at(0) != QLatin1String("cw") || parts.at(1) != QLatin1String("v1")) return false; bool okChapter = false, okFrac = false; const int ch = parts.at(2).toInt(&okChapter); const double fr = parts.at(3).toDouble(&okFrac); if (!okChapter || !okFrac) return false; if (chapterIndex) *chapterIndex = ch; if (fraction) *fraction = fr; return true; }