From f69650fb86f02132eb81c464cf304a48dbbd8dfa Mon Sep 17 00:00:00 2001 From: Leopoldo Pla Sempere Date: Mon, 25 May 2026 22:40:41 +0200 Subject: [PATCH] fix: validate OPF cover items as images (#2062) --- lib/Epub/Epub.cpp | 5 +- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 28 ++++++++++- lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 49 ++++++++++++++----- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index eb0f1b25..7ca320d5 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -104,8 +104,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) { const auto endPos = coverPageHtml.find('"', pos); if (endPos != std::string::npos) { const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos); - // Check if it's an image file - if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) { + // Cover BMP generation supports JPG/PNG only; skip GIF so an unsupported wrapper image + // does not block a later supported cover reference. + if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref)) { imageRef = ref; break; } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 1707b310..8f797561 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -5,12 +5,31 @@ #include #include +#include + #include "Epub/BookMetadataCache.h" namespace { constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml"; constexpr char MEDIA_TYPE_CSS[] = "text/css"; +constexpr char MEDIA_TYPE_IMAGE_PREFIX[] = "image/"; constexpr char itemCacheFile[] = "/.items.bin"; + +bool startsWithImageMediaType(const std::string& mediaType) { + constexpr size_t prefixLen = sizeof(MEDIA_TYPE_IMAGE_PREFIX) - 1; + if (mediaType.size() < prefixLen) { + return false; + } + + for (size_t i = 0; i < prefixLen; ++i) { + const char c = static_cast(std::tolower(static_cast(mediaType[i]))); + if (c != MEDIA_TYPE_IMAGE_PREFIX[i]) { + return false; + } + } + + return true; +} } // namespace bool ContentOpfParser::setup() { @@ -189,7 +208,14 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name serialization::writeString(self->tempItemStore, href); if (itemId == self->coverItemId) { - self->coverItemHref = href; + // Some EPUBs set meta name="cover" to an XHTML wrapper item. + // Only treat it as a cover image when the manifest media-type is image/*. + if (startsWithImageMediaType(mediaType)) { + self->coverItemHref = href; + } else { + LOG_DBG("COF", "Ignoring meta cover item '%s' with non-image media type: %s", itemId.c_str(), + mediaType.c_str()); + } } if (mediaType == MEDIA_TYPE_NCX) { diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 2b8f2b53..67116113 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -320,6 +320,14 @@ int bmpDrawCallback(JPEGDRAW* pDraw) { const int blockX = pDraw->x; const int blockY = pDraw->y; + // Guard against unexpected callback geometry so we never index past row buffers. + if (blockX < 0 || blockY < 0 || blockX >= ctx->srcWidth || blockY >= ctx->srcHeight) { + LOG_ERR("JPG", "Unexpected JPEG block origin (%d,%d) for decode grid %dx%d", blockX, blockY, ctx->srcWidth, + ctx->srcHeight); + ctx->error = true; + return 0; + } + // Copy block pixels into MCU row buffer for (int r = 0; r < blockH && r < MAX_MCU_HEIGHT; r++) { const int copyW = (blockX + validW <= ctx->srcWidth) ? validW : (ctx->srcWidth - blockX); @@ -403,8 +411,16 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b const int srcWidth = jpeg->getWidth(); const int srcHeight = jpeg->getHeight(); + const bool progressiveDecode = (jpeg->getJPEGType() == JPEG_MODE_PROGRESSIVE); + // JPEGDEC forces progressive streams to JPEG_SCALE_EIGHTH in DecodeJPEG, + // so callback coordinates and MCU buffering must use the reduced decode grid. + const int decodedSrcWidth = progressiveDecode ? ((srcWidth + 7) >> 3) : srcWidth; + const int decodedSrcHeight = progressiveDecode ? ((srcHeight + 7) >> 3) : srcHeight; LOG_DBG("JPG", "JPEG dimensions: %dx%d", srcWidth, srcHeight); + if (progressiveDecode) { + LOG_DBG("JPG", "Progressive JPEG decode uses 1/8 source: %dx%d", decodedSrcWidth, decodedSrcHeight); + } constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_HEIGHT = 3072; @@ -418,6 +434,15 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b // Calculate output dimensions (pre-scale to fit display exactly) int outWidth = srcWidth; int outHeight = srcHeight; + if (targetWidth <= 0 || targetHeight <= 0) { + // Without an explicit target, keep decoder-native dimensions. + outWidth = decodedSrcWidth; + outHeight = decodedSrcHeight; + } + + const int scaleSrcWidth = decodedSrcWidth; + const int scaleSrcHeight = decodedSrcHeight; + uint32_t scaleX_fp = 65536; // 1.0 in 16.16 fixed point uint32_t scaleY_fp = 65536; bool needsScaling = false; @@ -437,12 +462,14 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b if (outWidth < 1) outWidth = 1; if (outHeight < 1) outHeight = 1; - scaleX_fp = (static_cast(srcWidth) << 16) / outWidth; - scaleY_fp = (static_cast(srcHeight) << 16) / outHeight; - needsScaling = true; + LOG_DBG("JPG", "Scaling source %dx%d (decode grid %dx%d) -> %dx%d (target %dx%d)", srcWidth, srcHeight, + scaleSrcWidth, scaleSrcHeight, outWidth, outHeight, targetWidth, targetHeight); + } - LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", srcWidth, srcHeight, outWidth, outHeight, targetWidth, - targetHeight); + if (scaleSrcWidth != outWidth || scaleSrcHeight != outHeight) { + scaleX_fp = (static_cast(scaleSrcWidth) << 16) / outWidth; + scaleY_fp = (static_cast(scaleSrcHeight) << 16) / outHeight; + needsScaling = true; } // Write BMP header with output dimensions @@ -460,8 +487,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b BmpConvertCtx ctx = {}; ctx.bmpOut = &bmpOut; - ctx.srcWidth = srcWidth; - ctx.srcHeight = srcHeight; + ctx.srcWidth = scaleSrcWidth; + ctx.srcHeight = scaleSrcHeight; ctx.outWidth = outWidth; ctx.outHeight = outHeight; ctx.oneBit = oneBit; @@ -471,13 +498,13 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b ctx.scaleY_fp = scaleY_fp; ctx.error = false; - // MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale - ctx.mcuBuf = makeUniqueNoThrow(MAX_MCU_HEIGHT * srcWidth); + // MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale + ctx.mcuBuf = makeUniqueNoThrow(MAX_MCU_HEIGHT * ctx.srcWidth); if (!ctx.mcuBuf) { - LOG_ERR("JPG", "OOM: MCU buffer (%d bytes)", MAX_MCU_HEIGHT * srcWidth); + LOG_ERR("JPG", "OOM: MCU buffer (%d bytes)", MAX_MCU_HEIGHT * ctx.srcWidth); return false; } - memset(ctx.mcuBuf.get(), 0, MAX_MCU_HEIGHT * srcWidth); + memset(ctx.mcuBuf.get(), 0, MAX_MCU_HEIGHT * ctx.srcWidth); ctx.bmpRow = makeUniqueNoThrow(bytesPerRow); if (!ctx.bmpRow) {