fix: validate OPF cover items as images (#2062)
This commit is contained in:
+3
-2
@@ -104,8 +104,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
|||||||
const auto endPos = coverPageHtml.find('"', pos);
|
const auto endPos = coverPageHtml.find('"', pos);
|
||||||
if (endPos != std::string::npos) {
|
if (endPos != std::string::npos) {
|
||||||
const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
|
const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
|
||||||
// Check if it's an image file
|
// Cover BMP generation supports JPG/PNG only; skip GIF so an unsupported wrapper image
|
||||||
if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) {
|
// does not block a later supported cover reference.
|
||||||
|
if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref)) {
|
||||||
imageRef = ref;
|
imageRef = ref;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,31 @@
|
|||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <XmlParserUtils.h>
|
#include <XmlParserUtils.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "Epub/BookMetadataCache.h"
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||||
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
||||||
|
constexpr char MEDIA_TYPE_IMAGE_PREFIX[] = "image/";
|
||||||
constexpr char itemCacheFile[] = "/.items.bin";
|
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<char>(std::tolower(static_cast<unsigned char>(mediaType[i])));
|
||||||
|
if (c != MEDIA_TYPE_IMAGE_PREFIX[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool ContentOpfParser::setup() {
|
bool ContentOpfParser::setup() {
|
||||||
@@ -189,7 +208,14 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
serialization::writeString(self->tempItemStore, href);
|
serialization::writeString(self->tempItemStore, href);
|
||||||
|
|
||||||
if (itemId == self->coverItemId) {
|
if (itemId == self->coverItemId) {
|
||||||
|
// 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;
|
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) {
|
if (mediaType == MEDIA_TYPE_NCX) {
|
||||||
|
|||||||
@@ -320,6 +320,14 @@ int bmpDrawCallback(JPEGDRAW* pDraw) {
|
|||||||
const int blockX = pDraw->x;
|
const int blockX = pDraw->x;
|
||||||
const int blockY = pDraw->y;
|
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
|
// Copy block pixels into MCU row buffer
|
||||||
for (int r = 0; r < blockH && r < MAX_MCU_HEIGHT; r++) {
|
for (int r = 0; r < blockH && r < MAX_MCU_HEIGHT; r++) {
|
||||||
const int copyW = (blockX + validW <= ctx->srcWidth) ? validW : (ctx->srcWidth - blockX);
|
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 srcWidth = jpeg->getWidth();
|
||||||
const int srcHeight = jpeg->getHeight();
|
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);
|
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_WIDTH = 2048;
|
||||||
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
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)
|
// Calculate output dimensions (pre-scale to fit display exactly)
|
||||||
int outWidth = srcWidth;
|
int outWidth = srcWidth;
|
||||||
int outHeight = srcHeight;
|
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 scaleX_fp = 65536; // 1.0 in 16.16 fixed point
|
||||||
uint32_t scaleY_fp = 65536;
|
uint32_t scaleY_fp = 65536;
|
||||||
bool needsScaling = false;
|
bool needsScaling = false;
|
||||||
@@ -437,12 +462,14 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
|||||||
if (outWidth < 1) outWidth = 1;
|
if (outWidth < 1) outWidth = 1;
|
||||||
if (outHeight < 1) outHeight = 1;
|
if (outHeight < 1) outHeight = 1;
|
||||||
|
|
||||||
scaleX_fp = (static_cast<uint32_t>(srcWidth) << 16) / outWidth;
|
LOG_DBG("JPG", "Scaling source %dx%d (decode grid %dx%d) -> %dx%d (target %dx%d)", srcWidth, srcHeight,
|
||||||
scaleY_fp = (static_cast<uint32_t>(srcHeight) << 16) / outHeight;
|
scaleSrcWidth, scaleSrcHeight, outWidth, outHeight, targetWidth, targetHeight);
|
||||||
needsScaling = true;
|
}
|
||||||
|
|
||||||
LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", srcWidth, srcHeight, outWidth, outHeight, targetWidth,
|
if (scaleSrcWidth != outWidth || scaleSrcHeight != outHeight) {
|
||||||
targetHeight);
|
scaleX_fp = (static_cast<uint32_t>(scaleSrcWidth) << 16) / outWidth;
|
||||||
|
scaleY_fp = (static_cast<uint32_t>(scaleSrcHeight) << 16) / outHeight;
|
||||||
|
needsScaling = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write BMP header with output dimensions
|
// Write BMP header with output dimensions
|
||||||
@@ -460,8 +487,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
|||||||
|
|
||||||
BmpConvertCtx ctx = {};
|
BmpConvertCtx ctx = {};
|
||||||
ctx.bmpOut = &bmpOut;
|
ctx.bmpOut = &bmpOut;
|
||||||
ctx.srcWidth = srcWidth;
|
ctx.srcWidth = scaleSrcWidth;
|
||||||
ctx.srcHeight = srcHeight;
|
ctx.srcHeight = scaleSrcHeight;
|
||||||
ctx.outWidth = outWidth;
|
ctx.outWidth = outWidth;
|
||||||
ctx.outHeight = outHeight;
|
ctx.outHeight = outHeight;
|
||||||
ctx.oneBit = oneBit;
|
ctx.oneBit = oneBit;
|
||||||
@@ -471,13 +498,13 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
|||||||
ctx.scaleY_fp = scaleY_fp;
|
ctx.scaleY_fp = scaleY_fp;
|
||||||
ctx.error = false;
|
ctx.error = false;
|
||||||
|
|
||||||
// MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale
|
// MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale
|
||||||
ctx.mcuBuf = makeUniqueNoThrow<uint8_t[]>(MAX_MCU_HEIGHT * srcWidth);
|
ctx.mcuBuf = makeUniqueNoThrow<uint8_t[]>(MAX_MCU_HEIGHT * ctx.srcWidth);
|
||||||
if (!ctx.mcuBuf) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
memset(ctx.mcuBuf.get(), 0, MAX_MCU_HEIGHT * srcWidth);
|
memset(ctx.mcuBuf.get(), 0, MAX_MCU_HEIGHT * ctx.srcWidth);
|
||||||
|
|
||||||
ctx.bmpRow = makeUniqueNoThrow<uint8_t[]>(bytesPerRow);
|
ctx.bmpRow = makeUniqueNoThrow<uint8_t[]>(bytesPerRow);
|
||||||
if (!ctx.bmpRow) {
|
if (!ctx.bmpRow) {
|
||||||
|
|||||||
Reference in New Issue
Block a user